diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 00000000..b0eb57f4 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,4 @@ +# TODO: we shouldn't check this in to git, need to figure out how to avoid doing +# that. +[target.wasm32-unknown-unknown] +runner = 'cargo +nightly run --release -p wasm-bindgen-test-runner --' diff --git a/.travis.yml b/.travis.yml index 8df77d90..8e50ad71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -74,7 +74,9 @@ matrix: before_install: *INSTALL_NODE_VIA_NVM install: - npm ci --verbose - script: cargo test --manifest-path crates/js-sys/Cargo.toml + script: + - cargo test -p js-sys + - cargo test -p js-sys --target wasm32-unknown-unknown addons: firefox: latest diff --git a/Cargo.toml b/Cargo.toml index d8b4b6c8..ec8cf72a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,8 @@ wasm-bindgen-test-project-builder = { path = "crates/test-project-builder", vers members = [ "crates/cli", "crates/js-sys", + "crates/test", + "crates/test-runner", "crates/typescript", "crates/web-sys", "crates/webidl", @@ -58,7 +60,5 @@ members = [ "examples/comments" ] -[profile.release] -lto = true -opt-level = 's' -panic = 'abort' +[patch.crates-io] +wasm-bindgen = { path = '.' } diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 393e8be5..a4125f19 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -949,6 +949,27 @@ impl<'a> Context<'a> { self.pass_array_to_wasm("passArrayF64ToWasm", "getFloat64Memory", 8) } + fn expose_pass_array_jsvalue_to_wasm(&mut self) -> Result<(), Error> { + if !self.exposed_globals.insert("pass_array_jsvalue") { + return Ok(()); + } + self.require_internal_export("__wbindgen_malloc")?; + self.expose_uint32_memory(); + self.expose_add_heap_object(); + self.global(" + function passArrayJsValueToWasm(array) { + const ptr = wasm.__wbindgen_malloc(array.length * 4); + const mem = getUint32Memory(); + for (let i = 0; i < array.length; i++) { + mem[ptr / 4 + i] = addHeapObject(array[i]); + } + return [ptr, array.length]; + } + + "); + Ok(()) + } + fn pass_array_to_wasm( &mut self, name: &'static str, @@ -1387,7 +1408,10 @@ impl<'a> Context<'a> { self.expose_pass_array_f64_to_wasm()?; "passArrayF64ToWasm" } - VectorKind::Anyref => bail!("cannot pass list of JsValue to wasm yet"), + VectorKind::Anyref => { + self.expose_pass_array_jsvalue_to_wasm()?; + "passArrayJsValueToWasm" + } }; Ok(s) } diff --git a/crates/js-sys/Cargo.toml b/crates/js-sys/Cargo.toml index 37aac532..aa9b335b 100644 --- a/crates/js-sys/Cargo.toml +++ b/crates/js-sys/Cargo.toml @@ -19,5 +19,8 @@ doctest = false [dependencies] wasm-bindgen = { path = "../..", version = "0.2.12" } -[dev-dependencies] +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = { path = '../test', version = '=0.2.12' } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] wasm-bindgen-test-project-builder = { path = "../test-project-builder", version = '=0.2.12' } diff --git a/crates/js-sys/src/lib.rs b/crates/js-sys/src/lib.rs index 4dced3d1..fc1a8ef6 100644 --- a/crates/js-sys/src/lib.rs +++ b/crates/js-sys/src/lib.rs @@ -118,6 +118,15 @@ extern "C" { extern "C" { pub type Array; + /// Creates a new empty array + #[wasm_bindgen(constructor)] + pub fn new() -> Array; + + /// The `Array.from()` method creates a new, shallow-copied `Array` instance + /// from an array-like or iterable object. + #[wasm_bindgen(static_method_of = Array)] + pub fn from(val: JsValue) -> Array; + /// The copyWithin() method shallow copies part of an array to another /// location in the same array and returns it, without modifying its size. /// @@ -720,6 +729,43 @@ extern "C" { /// http://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/fill #[wasm_bindgen(method)] pub fn fill(this: &Int8Array, value: i8, start: u32, end: u32) -> Int8Array; + + /// The `buffer` accessor property represents the `ArrayBuffer` referenced + /// by a `TypedArray` at construction time. + #[wasm_bindgen(getter, method)] + pub fn buffer(this: &Int8Array) -> ArrayBuffer; + + // /// The `byteLength` accessor property represents the length (in bytes) of a + // /// typed array. + // #[wasm_bindgen(getter, method, js_name = byteLength)] + // pub fn byte_length(this: &Int8Array) -> u32; + // + // /// The `byteOffset` accessor property represents the offset (in bytes) of a + // /// typed array from the start of its `ArrayBuffer`. + // #[wasm_bindgen(getter, method, js_name = byteOffset)] + // pub fn byte_offset(this: &Int8Array) -> u32; + // + // /// The `length` accessor property represents the length (in elements) of a + // /// typed array. + // #[wasm_bindgen(getter, method)] + // pub fn length(this: &Int8Array) -> u32; + // + // /// The `set()` method stores multiple values in the typed array, reading + // /// input values from a specified array. + // #[wasm_bindgen(method)] + // pub fn set(this: &Int8Array, value: &JsValue, offset: u32); + + /// The `subarray()` method stores multiple values in the typed array, + /// reading input values from a specified array. + #[wasm_bindgen(method)] + pub fn subarray(this: &Int8Array, begin: u32, end: u32) -> Int8Array; + + /// The `forEach()` method executes a provided function once per array + /// element. This method has the same algorithm as + /// `Array.prototype.forEach()`. `TypedArray` is one of the typed array + /// types here. + #[wasm_bindgen(method, js_name = forEach)] + pub fn for_each(this: &Int8Array, callback: &mut FnMut(i8, u32, Int8Array)); } // Int16Array diff --git a/crates/js-sys/tests/all/ArrayBuffer.rs b/crates/js-sys/tests/all/ArrayBuffer.rs deleted file mode 100644 index f3eb046f..00000000 --- a/crates/js-sys/tests/all/ArrayBuffer.rs +++ /dev/null @@ -1,114 +0,0 @@ -#![allow(non_snake_case)] - -use super::project; - -#[test] -fn new() { - project() - .file("src/lib.rs", r#" - #![feature(use_extern_macros)] - - extern crate wasm_bindgen; - extern crate js_sys; - use wasm_bindgen::prelude::*; - use js_sys::ArrayBuffer; - - #[wasm_bindgen] - pub fn new_arraybuffer() -> ArrayBuffer { - ArrayBuffer::new(42) - } - "#) - .file("test.js", r#" - import * as assert from "assert"; - import * as wasm from "./out"; - - export function test() { - assert.equal(typeof wasm.new_arraybuffer(), "object"); - } - "#) - .test() -} - -#[test] -fn is_view() { - project() - .file("src/lib.rs", r#" - #![feature(use_extern_macros)] - - extern crate wasm_bindgen; - extern crate js_sys; - use JsValue; - use wasm_bindgen::prelude::*; - use js_sys::ArrayBuffer; - - #[wasm_bindgen] - pub fn is_view(value: JsValue) -> bool { - ArrayBuffer::is_view(value) - } - "#) - .file("test.js", r#" - import * as assert from "assert"; - import * as wasm from "./out"; - - export function test() { - assert.equal(wasm.is_view(new Uint8Array(42)), true); - } - "#) - .test() -} - -#[test] -fn slice() { - project() - .file("src/lib.rs", r#" - #![feature(use_extern_macros)] - - extern crate wasm_bindgen; - extern crate js_sys; - use wasm_bindgen::prelude::*; - use js_sys::ArrayBuffer; - - #[wasm_bindgen] - pub fn slice(arraybuffer: &ArrayBuffer, begin: u32) -> ArrayBuffer { - arraybuffer.slice(begin) - } - "#) - .file("test.js", r#" - import * as assert from "assert"; - import * as wasm from "./out"; - - export function test() { - const arraybuffer = new ArrayBuffer(4); - assert.equal(typeof wasm.slice(arraybuffer, 2), "object"); - } - "#) - .test() -} - -#[test] -fn slice_with_end() { - project() - .file("src/lib.rs", r#" - #![feature(use_extern_macros)] - - extern crate wasm_bindgen; - extern crate js_sys; - use wasm_bindgen::prelude::*; - use js_sys::ArrayBuffer; - - #[wasm_bindgen] - pub fn slice_with_end(arraybuffer: &ArrayBuffer, begin: u32, end: u32) -> ArrayBuffer { - arraybuffer.slice_with_end(begin, end) - } - "#) - .file("test.js", r#" - import * as assert from "assert"; - import * as wasm from "./out"; - - export function test() { - const arraybuffer = new ArrayBuffer(4); - assert.equal(typeof wasm.slice_with_end(arraybuffer, 1, 2), "object"); - } - "#) - .test() -} diff --git a/crates/js-sys/tests/all/ArrayIterator.rs b/crates/js-sys/tests/all/ArrayIterator.rs index 64d8b1d2..a18b5ad4 100644 --- a/crates/js-sys/tests/all/ArrayIterator.rs +++ b/crates/js-sys/tests/all/ArrayIterator.rs @@ -1,84 +1,6 @@ #![allow(non_snake_case)] -use project; - -#[test] -fn keys() { - project() - .file( - "src/lib.rs", - r#" - #![feature(use_extern_macros)] - - extern crate wasm_bindgen; - extern crate js_sys; - use wasm_bindgen::prelude::*; - - #[wasm_bindgen] - pub fn get_keys(this: &js_sys::Array) -> js_sys::ArrayIterator { - this.keys() - } - - "#, - ) - .file( - "test.js", - r#" - import * as assert from "assert"; - import * as wasm from "./out"; - - export function test() { - let numbers = [8, 5, 4, 3, 1, 2]; - let iterator = numbers.keys(); - let wasmIterator = wasm.get_keys(numbers); - - assert.equal(iterator.toString(), wasmIterator.toString()); - assert.equal(Array.from(iterator)[0], Array.from(wasmIterator)[0]); - } - "#, - ) - .test() -} - -#[test] -fn entries() { - project() - .file( - "src/lib.rs", - r#" - #![feature(use_extern_macros)] - - extern crate wasm_bindgen; - extern crate js_sys; - use wasm_bindgen::prelude::*; - - #[wasm_bindgen] - pub fn get_entries(this: &js_sys::Array) -> js_sys::ArrayIterator { - this.entries() - } - - "#, - ) - .file( - "test.js", - r#" - import * as assert from "assert"; - import * as wasm from "./out"; - - export function test() { - let numbers = [8, 5, 4, 3, 1, 2]; - let iterator = numbers.entries(); - let wasmIterator = wasm.get_entries(numbers); - let jsItem = iterator.next(); - let wasmItem = wasmIterator.next(); - - assert.equal(iterator.toString(), wasmIterator.toString()); - assert.equal(jsItem.value[1], wasmItem.value[1]); - } - "#, - ) - .test() -} +use super::project; #[test] fn values() { diff --git a/crates/js-sys/tests/all/Boolean.rs b/crates/js-sys/tests/all/Boolean.rs deleted file mode 100644 index 24c86d61..00000000 --- a/crates/js-sys/tests/all/Boolean.rs +++ /dev/null @@ -1,67 +0,0 @@ -#![allow(non_snake_case)] - -use project; - -#[test] -fn new_undefined() { - project() - .file( - "src/lib.rs", - r#" - #![feature(use_extern_macros)] - - extern crate wasm_bindgen; - extern crate js_sys; - use wasm_bindgen::prelude::*; - - #[wasm_bindgen] - pub fn new_boolean() -> js_sys::Boolean { - js_sys::Boolean::new(JsValue::undefined()) - } - "#, - ) - .file( - "test.js", - r#" - import * as assert from "assert"; - import * as wasm from "./out"; - - export function test() { - assert.equal(wasm.new_boolean().valueOf(), false); - } - "#, - ) - .test() -} - -#[test] -fn new_truely() { - project() - .file( - "src/lib.rs", - r#" - #![feature(use_extern_macros)] - - extern crate wasm_bindgen; - extern crate js_sys; - use wasm_bindgen::prelude::*; - - #[wasm_bindgen] - pub fn new_boolean() -> js_sys::Boolean { - js_sys::Boolean::new(JsValue::from("foo")) - } - "#, - ) - .file( - "test.js", - r#" - import * as assert from "assert"; - import * as wasm from "./out"; - - export function test() { - assert.equal(wasm.new_boolean().valueOf(), true); - } - "#, - ) - .test() -} diff --git a/crates/js-sys/tests/all/DataView.rs b/crates/js-sys/tests/all/DataView.rs deleted file mode 100644 index 03d433f7..00000000 --- a/crates/js-sys/tests/all/DataView.rs +++ /dev/null @@ -1,56 +0,0 @@ -#![allow(non_snake_case)] - -use super::project; - -#[test] -fn test() { - project() - .file("src/lib.rs", r#" - #![feature(use_extern_macros)] - - extern crate wasm_bindgen; - extern crate js_sys; - use wasm_bindgen::prelude::*; - use js_sys::{ArrayBuffer, DataView}; - - #[wasm_bindgen] - pub fn test_data_view(buffer: &ArrayBuffer, offset: usize, len: usize) { - let v = DataView::new(buffer, offset, len); - assert_eq!(v.byte_offset(), offset); - assert_eq!(v.byte_length(), len); - assert_eq!(v.get_int8(0), 2); - assert_eq!(v.get_uint8(0), 2); - - v.set_int8(0, 42); - assert_eq!(v.get_int8(0), 42); - v.set_uint8(0, 255); - assert_eq!(v.get_uint8(0), 255); - v.set_int16(0, 32767); - assert_eq!(v.get_int16(0), 32767); - v.set_uint16(0, 65535); - assert_eq!(v.get_uint16(0), 65535); - v.set_int32(0, 123456789); - assert_eq!(v.get_int32(0), 123456789); - v.set_uint32(0, 3_123_456_789); - assert_eq!(v.get_uint32(0), 3_123_456_789); - v.set_float32(0, 100.123); - assert_eq!(v.get_float32(0), 100.123); - v.set_float64(0, 123456789.123456); - assert_eq!(v.get_float64(0), 123456789.123456); - - v.set_int8(0, 42); - } - "#) - .file("test.js", r#" - import * as assert from "assert"; - import * as wasm from "./out"; - - export function test() { - const bytes = new Int8Array(10); - bytes[2] = 2; - wasm.test_data_view(bytes.buffer, 2, 8); - assert.equal(bytes[2], 42); - } - "#) - .test() -} diff --git a/crates/js-sys/tests/all/main.rs b/crates/js-sys/tests/all/main.rs index a0e126c2..9a198062 100644 --- a/crates/js-sys/tests/all/main.rs +++ b/crates/js-sys/tests/all/main.rs @@ -1,3 +1,5 @@ +#![cfg(not(target_arch = "wasm32"))] + extern crate wasm_bindgen_test_project_builder as project_builder; fn project() -> project_builder::Project { @@ -9,10 +11,7 @@ fn project() -> project_builder::Project { // Keep these tests in alphabetical order, just like the imports in `src/js.rs`. mod Array; -mod ArrayBuffer; mod ArrayIterator; -mod Boolean; -mod DataView; mod Date; mod Error; mod Function; diff --git a/crates/js-sys/tests/wasm/ArrayBuffer.rs b/crates/js-sys/tests/wasm/ArrayBuffer.rs new file mode 100644 index 00000000..f803698f --- /dev/null +++ b/crates/js-sys/tests/wasm/ArrayBuffer.rs @@ -0,0 +1,30 @@ +use wasm_bindgen::JsValue; +use wasm_bindgen_test::*; +use js_sys::*; + +#[wasm_bindgen_test] +fn new() { + let x = ArrayBuffer::new(42); + let y: JsValue = x.into(); + assert!(y.is_object()); +} + +#[wasm_bindgen_test] +fn is_view() { + let x = Uint8Array::new(JsValue::from(42)); + assert!(ArrayBuffer::is_view(JsValue::from(x))); +} + +#[test] +fn slice() { + let buf = ArrayBuffer::new(4); + let slice = buf.slice(2); + assert!(JsValue::from(slice).is_object()); +} + +#[test] +fn slice_with_end() { + let buf = ArrayBuffer::new(4); + let slice = buf.slice_with_end(1, 2); + assert!(JsValue::from(slice).is_object()); +} diff --git a/crates/js-sys/tests/wasm/ArrayIterator.rs b/crates/js-sys/tests/wasm/ArrayIterator.rs new file mode 100644 index 00000000..1304b551 --- /dev/null +++ b/crates/js-sys/tests/wasm/ArrayIterator.rs @@ -0,0 +1,39 @@ +use wasm_bindgen::JsValue; +use wasm_bindgen_test::*; +use js_sys::*; + +#[wasm_bindgen_test] +fn keys() { + let array = Array::new(); + array.push(JsValue::from(1)); + array.push(JsValue::from(2)); + array.push(JsValue::from(3)); + array.push(JsValue::from(4)); + array.push(JsValue::from(5)); + + let new_array = Array::from(array.keys().into()); + + let mut result = Vec::new(); + new_array.for_each(&mut |i, _, _| result.push(i.as_f64().unwrap())); + assert_eq!(result, [0.0, 1.0, 2.0, 3.0, 4.0]); +} + +#[wasm_bindgen_test] +fn entries() { + let array = Array::new(); + array.push(JsValue::from(1)); + array.push(JsValue::from(2)); + array.push(JsValue::from(3)); + array.push(JsValue::from(4)); + array.push(JsValue::from(5)); + + let new_array = Array::from(array.entries().into()); + + new_array.for_each(&mut |a, i, _| { + assert!(a.is_object()); + let array: Array = a.into(); + assert_eq!(array.shift().as_f64().unwrap(), i as f64); + assert_eq!(array.shift().as_f64().unwrap(), (i + 1) as f64); + assert_eq!(array.length(), 0); + }); +} diff --git a/crates/js-sys/tests/wasm/Boolean.rs b/crates/js-sys/tests/wasm/Boolean.rs new file mode 100644 index 00000000..c9f21759 --- /dev/null +++ b/crates/js-sys/tests/wasm/Boolean.rs @@ -0,0 +1,13 @@ +use wasm_bindgen::JsValue; +use wasm_bindgen_test::*; +use js_sys::*; + +#[wasm_bindgen_test] +fn new_undefined() { + assert_eq!(Boolean::new(JsValue::undefined()).value_of(), false); +} + +#[wasm_bindgen_test] +fn new_truely() { + assert_eq!(Boolean::new(JsValue::from("foo")).value_of(), true); +} diff --git a/crates/js-sys/tests/wasm/DataView.rs b/crates/js-sys/tests/wasm/DataView.rs new file mode 100644 index 00000000..9dd8baf4 --- /dev/null +++ b/crates/js-sys/tests/wasm/DataView.rs @@ -0,0 +1,39 @@ +use wasm_bindgen::JsValue; +use wasm_bindgen_test::*; +use js_sys::*; + +#[wasm_bindgen_test] +fn test() { + let bytes = Int8Array::new(JsValue::from(10)); + + // TODO: figure out how to do `bytes[2] = 2` + bytes.subarray(2, 3).fill(2, 0, 1); + + let v = DataView::new(&bytes.buffer(), 2, 8); + assert_eq!(v.byte_offset(), 2); + assert_eq!(v.byte_length(), 8); + assert_eq!(v.get_int8(0), 2); + assert_eq!(v.get_uint8(0), 2); + + v.set_int8(0, 42); + assert_eq!(v.get_int8(0), 42); + v.set_uint8(0, 255); + assert_eq!(v.get_uint8(0), 255); + v.set_int16(0, 32767); + assert_eq!(v.get_int16(0), 32767); + v.set_uint16(0, 65535); + assert_eq!(v.get_uint16(0), 65535); + v.set_int32(0, 123456789); + assert_eq!(v.get_int32(0), 123456789); + v.set_uint32(0, 3_123_456_789); + assert_eq!(v.get_uint32(0), 3_123_456_789); + v.set_float32(0, 100.123); + assert_eq!(v.get_float32(0), 100.123); + v.set_float64(0, 123456789.123456); + assert_eq!(v.get_float64(0), 123456789.123456); + + v.set_int8(0, 42); + + // TODO: figure out how to do `bytes[2]` + bytes.subarray(2, 3).for_each(&mut |x, _, _| assert_eq!(x, 42)); +} diff --git a/crates/js-sys/tests/wasm/main.rs b/crates/js-sys/tests/wasm/main.rs new file mode 100644 index 00000000..d690c77e --- /dev/null +++ b/crates/js-sys/tests/wasm/main.rs @@ -0,0 +1,12 @@ +#![cfg(target_arch = "wasm32")] +#![feature(use_extern_macros, wasm_import_module)] +#![allow(non_snake_case)] + +extern crate js_sys; +extern crate wasm_bindgen; +extern crate wasm_bindgen_test; + +pub mod ArrayBuffer; +pub mod ArrayIterator; +pub mod Boolean; +pub mod DataView; diff --git a/crates/test-macro/Cargo.toml b/crates/test-macro/Cargo.toml new file mode 100644 index 00000000..a2a4ad5d --- /dev/null +++ b/crates/test-macro/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wasm-bindgen-test-macro" +version = "0.2.12" +authors = ["The wasm-bindgen Developers"] + +[dependencies] +proc-macro2 = { version = "0.4", features = ['nightly'] } +quote = "0.6" + +[lib] +proc-macro = true diff --git a/crates/test-macro/README.md b/crates/test-macro/README.md new file mode 100644 index 00000000..64f20b4a --- /dev/null +++ b/crates/test-macro/README.md @@ -0,0 +1,5 @@ +# wasm-bindgen-test-runner + +This is an **experimental** crate for enabling `cargo test --target +wasm32-unknown-unknown`. For more information see the README fo +`wasm-bindgen-test`. diff --git a/crates/test-macro/src/lib.rs b/crates/test-macro/src/lib.rs new file mode 100644 index 00000000..486f34a7 --- /dev/null +++ b/crates/test-macro/src/lib.rs @@ -0,0 +1,54 @@ +//! See the README for `wasm-bindgen-test` for a bit more info about what's +//! going on here. + +extern crate proc_macro; +extern crate proc_macro2; +#[macro_use] +extern crate quote; + +use std::sync::atomic::*; +use proc_macro2::*; + +static CNT: AtomicUsize = ATOMIC_USIZE_INIT; + +#[proc_macro_attribute] +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 body = TokenStream::from(body).into_iter(); + + // Assume the input item is of the form `fn #ident ...`, and extract + // `#ident` + let fn_tok = body.next(); + let ident = match body.next() { + Some(TokenTree::Ident(token)) => token, + _ => panic!("expected a function name"), + }; + + let mut tokens = Vec::::new(); + + // 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. + let name = format!("__wbg_test_{}_{}", ident, CNT.fetch_add(1, Ordering::SeqCst)); + let name = Ident::new(&name, Span::call_site()); + tokens.extend((quote! { + #[no_mangle] + pub extern fn #name(cx: *const ::wasm_bindgen_test::__rt::Context) { + unsafe { + (*cx).execute(concat!(module_path!(), "::", stringify!(#ident)), #ident); + } + } + }).into_iter()); + + tokens.extend(fn_tok); + tokens.push(ident.into()); + tokens.extend(body); + + tokens.into_iter().collect::().into() +} diff --git a/crates/test-runner/Cargo.toml b/crates/test-runner/Cargo.toml new file mode 100644 index 00000000..7e4344cb --- /dev/null +++ b/crates/test-runner/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "wasm-bindgen-test-runner" +version = "0.2.12" +authors = ["The wasm-bindgen Developers"] + +[dependencies] +wasm-bindgen-cli-support = { path = '../cli-support', version = '=0.2.12' } +failure = "0.1" +parity-wasm = "0.31" diff --git a/crates/test-runner/README.md b/crates/test-runner/README.md new file mode 100644 index 00000000..64f20b4a --- /dev/null +++ b/crates/test-runner/README.md @@ -0,0 +1,5 @@ +# wasm-bindgen-test-runner + +This is an **experimental** crate for enabling `cargo test --target +wasm32-unknown-unknown`. For more information see the README fo +`wasm-bindgen-test`. diff --git a/crates/test-runner/src/main.rs b/crates/test-runner/src/main.rs new file mode 100644 index 00000000..32f9a7c7 --- /dev/null +++ b/crates/test-runner/src/main.rs @@ -0,0 +1,166 @@ +#[macro_use] +extern crate failure; +extern crate wasm_bindgen_cli_support; +extern crate parity_wasm; + +use std::env; +use std::fs::{self, File}; +use std::io::{Write, Read}; +use std::path::PathBuf; +use std::process::{self, Command}; + +use failure::{ResultExt, Error}; +use parity_wasm::elements::{Module, Deserialize}; +use wasm_bindgen_cli_support::Bindgen; + +fn main() { + let err = match rmain() { + Ok(()) => return, + Err(e) => e, + }; + eprintln!("error: {}", err); + for cause in err.causes().skip(1) { + eprintln!("\tcaused by: {}", cause); + } + process::exit(1); +} + +fn rmain() -> Result<(), Error> { + let mut args = env::args_os().skip(1); + + // Currently no flags are supported, and assume there's only one argument + // which is the wasm file to test. This'll want to improve over time! + let wasm_file_to_test = match args.next() { + Some(file) => PathBuf::from(file), + None => bail!("must have a file to test as first argument"), + }; + + // Assume a cargo-like directory layout and generate output at + // `target/wasm32-unknown-unknown/wbg-tmp/...` + let tmpdir = wasm_file_to_test.parent() // chop off file name + .and_then(|p| p.parent()) // chop off `deps` + .and_then(|p| p.parent()) // chop off `debug` + .map(|p| p.join("wbg-tmp")) + .ok_or_else(|| { + format_err!("file to test doesn't follow the expected Cargo conventions") + })?; + + // Make sure there's no stale state from before + drop(fs::remove_dir_all(&tmpdir)); + fs::create_dir(&tmpdir) + .context("creating temporary directory")?; + + // For now unconditionally generate wasm-bindgen code tailored for node.js, + // but eventually we'll want more options here for browsers! + let mut b = Bindgen::new(); + b.debug(true) + .nodejs(true) + .input_path(&wasm_file_to_test) + .keep_debug(false) + .generate(&tmpdir) + .context("executing `wasm-bindgen` over the wasm file")?; + + let module = wasm_file_to_test.file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| format_err!("invalid filename passed in"))?; + + let mut js_to_execute = format!(r#" + const {{ exit }} = require('process'); + + let cx = 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) {{ + prev_log.apply(null, arguments); + }} else {{ + cx.console_log(prev_log, arguments); + }} + }}; + const prev_error = console.error; + console.error = function() {{ + if (cx === null) {{ + prev_error.apply(null, arguments); + }} else {{ + cx.console_error(prev_error, arguments); + }} + }}; + + const support = require("./{0}"); + const wasm = require("./{0}_bg"); + + // Hack for now to support 0 tests in a binary. This should be done + // better... + if (support.Context === undefined) + process.exit(0); + + cx = new support.Context(); + + // Forward runtime arguments. These arguments are also arguments to the + // `wasm-bindgen-test-runner` which forwards them to node which we + // forward to the test harness. this is basically only used for test + // filters for now. + cx.args(process.argv.slice(2)); + + const tests = []; + "#, + module + ); + + // Collect all tests that the test harness is supposed to run. We assume + // that any exported function with the prefix `__wbg_test` is a test we need + // to execute. + // + // Note that we're collecting *JS objects* that represent the functions to + // execute, and then those objects are passed into wasm for it to execute + // when it sees fit. + let mut wasm = Vec::new(); + let wasm_file = tmpdir.join(format!("{}_bg.wasm", module)); + File::open(wasm_file).and_then(|mut f| f.read_to_end(&mut wasm)) + .context("failed to read wasm file")?; + let module = Module::deserialize(&mut &wasm[..]) + .context("failed to deserialize wasm module")?; + if let Some(exports) = module.export_section() { + for export in exports.entries() { + if !export.field().starts_with("__wbg_test") { + continue + } + js_to_execute.push_str(&format!("tests.push(wasm.{})\n", export.field())); + } + } + + // And as a final addendum, exit with a nonzero code if any tests fail. + js_to_execute.push_str("if (!cx.run(tests)) exit(1);\n"); + + let js_path = tmpdir.join("run.js"); + File::create(&js_path) + .and_then(|mut f| f.write_all(js_to_execute.as_bytes())) + .context("failed to write JS file")?; + + // Last but not least, execute `node`! Add an entry to `NODE_PATH` for the + // project root to hopefully pick up `node_modules` and other local imports. + let path = env::var_os("NODE_PATH").unwrap_or_default(); + let mut paths = env::split_paths(&path).collect::>(); + paths.push(env::current_dir().unwrap()); + exec( + Command::new("node") + .env("NODE_PATH", env::join_paths(&paths).unwrap()) + .arg(&js_path) + .args(args) + ) +} + +#[cfg(unix)] +fn exec(cmd: &mut Command) -> Result<(), Error> { + use std::os::unix::prelude::*; + Err(Error::from(cmd.exec()).context("failed to execute `node`").into()) +} + +#[cfg(windows)] +fn exec(cmd: &mut Command) -> Result<(), Error> { + let status = cmd.status()?; + process::exit(status.code().unwrap_or(3)); +} diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml new file mode 100644 index 00000000..c4b3813e --- /dev/null +++ b/crates/test/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "wasm-bindgen-test" +version = "0.2.12" +authors = ["The wasm-bindgen Developers"] + +[dependencies] +wasm-bindgen-test-macro = { path = '../test-macro', version = '=0.2.12' } +wasm-bindgen = { path = '../..', version = '0.2.12' } +js-sys = { path = '../js-sys', version = '0.2.12' } +console_error_panic_hook = '0.1' + +[lib] +test = false diff --git a/crates/test/README.md b/crates/test/README.md new file mode 100644 index 00000000..0540858c --- /dev/null +++ b/crates/test/README.md @@ -0,0 +1,170 @@ +# wasm-bindgen-test + +This crate is an experimental test harness for `wasm32-unknown-unknown`, with +the goal of allowing you to write tests as you normally do in Rust and then +simply: + +``` +cargo test --target wasm32-unknown-unknown +``` + +This project is still in the early stages of its development so there's not a +ton of documentation just yet, but a taste of how it works is: + +* First, install the test runner. + + ``` + cargo install --path crates/test-runner + ``` + +* Next, add this to your `.cargo/config`: + + ```toml + [target.wasm32-unknown-unknown] + runner = 'wasm-bindgen-test-runner' + ``` + +* Next, configure your project's dev-dependencies: + + ```toml + [dev-dependencies] + # or [target.'cfg(target_arch = "wasm32")'.dev-dependencies] + wasm-bindgen-test = { git = 'https://github.com/rustwasm/wasm-bindgen' } + ``` + +* Next, write some tests! + + ```rust + // in tests/wasm.rs + #![feature(use_extern_macros)] + + extern crate wasm_bindgen_test; + + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn pass() { + assert_eq!(1, 1); + } + + #[wasm_bindgen_test] + fn fail() { + assert_eq!(1, 2); + } + ``` + +* And finally, execute your tests: + + ``` + $ cargo test --target wasm32-unknown-unknown + Finished dev [unoptimized + debuginfo] target(s) in 0.11s + Running /home/.../target/wasm32-unknown-unknown/debug/deps/wasm-4a309ffe6ad80503.wasm + running 2 tests + + test wasm::pass ... ok + test wasm::fail ... FAILED + + failures: + + ---- wasm::fail output ---- + error output: + panicked at 'assertion failed: `(left == right)` + left: `1`, + right: `2`', crates/test/tests/wasm.rs:14:5 + + JS exception that was thrown: + RuntimeError: unreachable + at __rust_start_panic (wasm-function[1362]:33) + at rust_panic (wasm-function[1357]:30) + at std::panicking::rust_panic_with_hook::h56e5e464b0e7fc22 (wasm-function[1352]:444) + at std::panicking::continue_panic_fmt::had70ba48785b9a8f (wasm-function[1350]:122) + at std::panicking::begin_panic_fmt::h991e7d1ca9bf9c0c (wasm-function[1351]:95) + at wasm::fail::ha4c23c69dfa0eea9 (wasm-function[88]:477) + at core::ops::function::FnOnce::call_once::h633718dad359559a (wasm-function[21]:22) + at wasm_bindgen_test::__rt::Context::execute::h2f669104986475eb (wasm-function[13]:291) + at __wbg_test_fail_1 (wasm-function[87]:57) + at module.exports.__wbg_apply_2ba774592c5223a7 (/home/alex/code/wasm-bindgen/target/wasm32-unknown-unknown/wbg-tmp/wasm-4a309ffe6ad80503.js:61:66) + + + failures: + + wasm::fail + + test result: FAILED. 1 passed; 1 failed; 0 ignored + + error: test failed, to rerun pass '--test wasm' + ``` + +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. + +## Components + +The test harness is made of three separate components, but you typically don't +have to worry about most of them. They're documented here for documentation +purposes! + +### `wasm-bindgen-test-macro` + +This crate, living at `crates/test-macro`, is a procedural macro that defines +the `#[wasm_bindgen_test]` macro. **The normal `#[test]` cannot be used and will +not work.** Eventually it's intended that the `#[wasm_bindgen_test]` attribute +could gain arguments like "run in a browser" or something like a minimum Node +version. + +For now though the macro is pretty simple and reexported from the next crate, +`wasm-bindgen-test`. + +### `wasm-bindgen-test` + +This is the runtime support needed to execute tests. This is basically the same +thing as the `test` crate in the Rust repository, and one day it will likely use +the `test` crate itself! For now though it's a minimal reimplementation that +provides the support for: + +* Printing what test cases are running +* Collecting `console.log` and `console.error` output of each test case for + printing later +* Rendering the failure output of each test case +* Catching JS exceptions so tests can continue to run after a test fails +* Driving execution of all tests + +This is the crate which you actually link to in your wasm test and through which +you import the `#[wasm_bindgen_test]` macro. Otherwise this crate provides a +`console_log!` macro that's a utility like `println!` only using `console.log`. + +This crate may grow more functionality in the future, but for now it's somewhat +bare bones! + +### `wasm-bindgen-test-runner` + +This is where the secret sauce comes into play. We configured Cargo to execute +this binary *instead* of directly executing the `*.wasm` file (which Cargo would +otherwise try to do). This means that whenever a test is executed it executes +this binary with the wasm file as an argument, allowing it to take full control +over the test process! + +The test runner is currently pretty simple, executing a few steps: + +* First, it runs the equivalent of `wasm-bindgen`. This'll generate wasm-bindgen + output in a temoprary directory. +* Next, it generates a small shim JS file which imports these + wasm-bindgen-generated files and executes the test harness. +* Finally, it executes `node` over the generated JS file, executing all of your + tests. + +In essence what happens is that this test runner automatically executes +`wasm-bindgen` and then uses Node to actually execute the wasm file, meaning +that your wasm code currently runs in a Node environment. + +## Future Work + +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/out.sh b/crates/test/out.sh new file mode 100644 index 00000000..6c38aa77 --- /dev/null +++ b/crates/test/out.sh @@ -0,0 +1,29 @@ +rustc \ + +nightly \ + --crate-name \ + wasm_bindgen_test_runner \ + crates/test-runner/src/main.rs \ + --crate-type \ + bin \ + --emit=dep-info,link \ + -C \ + opt-level=s \ + -C \ + panic=abort \ + -C \ + lto \ + -C \ + metadata=de5c24b259631256 \ + -C \ + extra-filename=-de5c24b259631256 \ + --out-dir \ + /home/alex/code/wasm-bindgen/target/release/deps \ + -L \ + dependency=/home/alex/code/wasm-bindgen/target/release/deps \ + --extern \ + failure=/home/alex/code/wasm-bindgen/target/release/deps/libfailure-7c7642ad9a5ea78f.rlib \ + --extern \ + wasm_bindgen_cli_support=/home/alex/code/wasm-bindgen/target/release/deps/libwasm_bindgen_cli_support-319f8cb472c7d7e6.rlib \ + -L \ + native=/home/alex/code/wasm-bindgen/target/release/build/backtrace-sys-219bc8940e273b6b/out \ + -Z time-passes diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs new file mode 100644 index 00000000..afb93669 --- /dev/null +++ b/crates/test/src/lib.rs @@ -0,0 +1,25 @@ +//! Runtime support for the `#[wasm_bindgen_test]` attribute +//! +//! More documentation can be found in the README for this crate! + +#![feature(use_extern_macros, wasm_import_module)] + +extern crate wasm_bindgen_test_macro; +extern crate wasm_bindgen; +extern crate js_sys; +extern crate console_error_panic_hook; + +pub use wasm_bindgen_test_macro::wasm_bindgen_test; + +/// Helper macro which acts like `println!` only routes to `console.log` +/// instead. +#[macro_export] +macro_rules! console_log { + ($($arg:tt)*) => ( + $crate::__rt::log(&format_args!($($arg)*)) + ) +} + +#[path = "rt.rs"] +#[doc(hidden)] +pub mod __rt; diff --git a/crates/test/src/rt.rs b/crates/test/src/rt.rs new file mode 100644 index 00000000..4b4e8a72 --- /dev/null +++ b/crates/test/src/rt.rs @@ -0,0 +1,237 @@ +use std::cell::{RefCell, Cell}; +use std::fmt; +use std::mem; + +use console_error_panic_hook; +use js_sys::Array; +use wasm_bindgen::prelude::*; + +/// Runtime test harness support instantiated in JS. +/// +/// The node.js entry script instantiates a `Context` here which is used to +/// drive test execution. +#[wasm_bindgen] +pub struct Context { + filter: Option, + current_test: RefCell>, + succeeded: Cell, + ignored: Cell, + failures: RefCell>, + current_log: RefCell, + current_error: RefCell, + ignore_this_test: Cell, +} + +#[wasm_bindgen] +extern { + // Redefined from `js_sys` so we can catch the error + pub type Function; + #[wasm_bindgen(method, catch)] + fn apply(this: &Function, new_this: &JsValue, args: &Array) -> Result; + + #[wasm_bindgen(js_namespace = console, js_name = log)] + #[doc(hidden)] + pub fn console_log(s: &str); + + // Not using `js_sys::Error` because node's errors specifically have a + // `stack` attribute. + type NodeError; + #[wasm_bindgen(method, getter, js_class = "Error", structural)] + fn stack(this: &NodeError) -> String; + + // General-purpose conversion into a `String`. + #[wasm_bindgen(js_name = String)] + fn stringify(val: &JsValue) -> String; +} + +#[wasm_bindgen(module = "fs", version = "*")] +extern { + fn writeSync(fd: i32, data: &[u8]); +} + +pub fn log(args: &fmt::Arguments) { + console_log(&args.to_string()); +} + +#[wasm_bindgen] +impl Context { + #[wasm_bindgen(constructor)] + pub fn new() -> Context { + console_error_panic_hook::set_once(); + + 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), + } + } + + /// Inform this context about runtime arguments passed to the test + /// harness. + /// + /// Eventually this will be used to support flags, but for now it's just + /// used to support test filters. + pub fn args(&mut self, args: Vec) { + 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() { + panic!("more than one filter argument cannot be passed"); + } + self.filter = Some(arg); + } + } + + /// Executes a list of tests, returning whether any of them failed. + /// + /// 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(); + let args = Array::new(); + args.push(JsValue::from(self as *const Context as u32)); + + let noun = if tests.len() == 1 { "test" } else { "tests" }; + console_log!("running {} {}", tests.len(), noun); + console_log!(""); + + for test in tests { + self.ignore_this_test.set(false); + let test = Function::from(test); + match test.apply(&this, &args) { + Ok(_) => { + if self.ignore_this_test.get() { + self.log_ignore() + } else { + self.log_success() + } + } + Err(e) => self.log_error(e.into()), + } + 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 + } + + 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()); + let data = format!("test {} ... ", test); + writeSync(2, data.as_bytes()); + } + + fn log_success(&self) { + writeSync(2, b"ok\n"); + self.succeeded.set(self.succeeded.get() + 1); + } + + fn log_ignore(&self) { + writeSync(2, b"ignored\n"); + self.ignored.set(self.ignored.get() + 1); + } + + fn log_error(&self, err: NodeError) { + writeSync(2, b"FAILED\n"); + 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"); + } + if error.len() > 0 { + msg.push_str("error output:\n"); + msg.push_str(&tab(&error)); + msg.push_str("\n"); + } + msg.push_str("JS exception that was thrown:\n"); + msg.push_str(&tab(&err.stack())); + self.failures.borrow_mut().push((name, msg)); + } + + fn log_results(&self) { + let failures = self.failures.borrow(); + if failures.len() > 0 { + console_log!("\nfailures:\n"); + for (test, logs) in failures.iter() { + console_log!("---- {} output ----\n{}\n", test, tab(logs)); + } + console_log!("failures:\n"); + for (test, _) in failures.iter() { + console_log!(" {}\n", test); + } + } else { + console_log!(""); + } + console_log!( + "test result: {}. \ + {} passed; \ + {} failed; \ + {} ignored\n", + if failures.len() == 0 { "ok" } else { "FAILED" }, + self.succeeded.get(), + failures.len(), + self.ignored.get(), + ); + } + + pub fn console_log(&self, original: &Function, args: &Array) { + self.log(original, args, &self.current_log) + } + + 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 + } + 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"); + } +} + +impl Context { + 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(); + } +} + +fn tab(s: &str) -> String { + let mut result = String::new(); + for line in s.lines() { + result.push_str(" "); + result.push_str(line); + result.push_str("\n"); + } + return result; +}