mirror of
https://github.com/rustwasm/wasm-bindgen.git
synced 2024-12-28 04:22:38 +03:00
Update test harness for browser testing
This commit updates the test harness for in-browser testing. It now no longer unconditionally uses `fs.writeSync`, for example. Instead a `Formatter` trait is introduced for both Node/browser environments and at runtime we detect which is the appropriate one to use.
This commit is contained in:
parent
0770f830e7
commit
8fc40e4c0f
@ -20,6 +20,6 @@ macro_rules! console_log {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[path = "rt.rs"]
|
#[path = "rt/mod.rs"]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod __rt;
|
pub mod __rt;
|
||||||
|
111
crates/test/src/rt/browser.rs
Normal file
111
crates/test/src/rt/browser.rs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
//! Support for printing status information of a test suite in a browser.
|
||||||
|
//!
|
||||||
|
//! Currently this is quite simple, rendering the same as the console tests in
|
||||||
|
//! node.js. Output here is rendered in a `pre`, however.
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use js_sys::Error;
|
||||||
|
|
||||||
|
pub struct Browser {
|
||||||
|
pre: Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern {
|
||||||
|
type HTMLDocument;
|
||||||
|
static document: HTMLDocument;
|
||||||
|
#[wasm_bindgen(method, structural)]
|
||||||
|
fn getElementById(this: &HTMLDocument, id: &str) -> Element;
|
||||||
|
|
||||||
|
type Element;
|
||||||
|
#[wasm_bindgen(method, getter = innerHTML, structural)]
|
||||||
|
fn inner_html(this: &Element) -> String;
|
||||||
|
#[wasm_bindgen(method, setter = innerHTML, structural)]
|
||||||
|
fn set_inner_html(this: &Element, html: &str);
|
||||||
|
|
||||||
|
type BrowserError;
|
||||||
|
#[wasm_bindgen(method, getter, structural)]
|
||||||
|
fn stack(this: &BrowserError) -> JsValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Browser {
|
||||||
|
pub fn new() -> Browser {
|
||||||
|
let pre = document.getElementById("output");
|
||||||
|
pre.set_inner_html("");
|
||||||
|
Browser {
|
||||||
|
pre,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::Formatter for Browser {
|
||||||
|
fn writeln(&self, line: &str) {
|
||||||
|
let mut html = self.pre.inner_html();
|
||||||
|
html.push_str(&line);
|
||||||
|
html.push_str("\n");
|
||||||
|
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_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);
|
||||||
|
|
||||||
|
// TODO: this should be a checked cast to `Error`
|
||||||
|
let err = Error::from(err);
|
||||||
|
let name = String::from(err.name());
|
||||||
|
let message = String::from(err.message());
|
||||||
|
let err = BrowserError::from(JsValue::from(err));
|
||||||
|
let stack = err.stack();
|
||||||
|
|
||||||
|
let mut header = format!("{}: {}", name, message);
|
||||||
|
let stack = match stack.as_string() {
|
||||||
|
Some(stack) => stack,
|
||||||
|
None => return header,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the `stack` variable contains the name/message already, this is
|
||||||
|
// probably a chome-like error which is already rendered well, so just
|
||||||
|
// return this info
|
||||||
|
if stack.contains(&header) {
|
||||||
|
return stack
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a firefox-like error where all lines have a `@` in them
|
||||||
|
// separating the symbol and source
|
||||||
|
if stack.lines().all(|s| s.contains("@")) {
|
||||||
|
for line in stack.lines() {
|
||||||
|
header.push_str("\n");
|
||||||
|
header.push_str(" at");
|
||||||
|
for part in line.split("@") {
|
||||||
|
header.push_str(" ");
|
||||||
|
header.push_str(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to make sure we don't lose any info
|
||||||
|
format!("{}\n{}", header, stack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
62
crates/test/src/rt/detect.rs
Normal file
62
crates/test/src/rt/detect.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use js_sys::{Array, Function};
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern {
|
||||||
|
#[wasm_bindgen(js_name = Function)]
|
||||||
|
fn new_function(s: &str) -> Function;
|
||||||
|
|
||||||
|
type This;
|
||||||
|
#[wasm_bindgen(method, getter, structural, js_name = self)]
|
||||||
|
fn self_(me: &This) -> JsValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether it's likely we're executing in a browser environment, as
|
||||||
|
/// opposed to node.js.
|
||||||
|
pub fn is_browser() -> bool {
|
||||||
|
// This is a bit tricky to define. The basic crux of this is that we want to
|
||||||
|
// test if the `self` identifier is defined. That is defined in browsers
|
||||||
|
// (and web workers!) but not in Node. To that end you might expect:
|
||||||
|
//
|
||||||
|
// #[wasm_bindgen]
|
||||||
|
// extern {
|
||||||
|
// #[wasm_bindgen(js_name = self)]
|
||||||
|
// static SELF: JsValue;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// *SELF != JsValue::undefined()
|
||||||
|
//
|
||||||
|
// this currently, however, throws a "not defined" error in JS because the
|
||||||
|
// generated function looks like `function() { return self; }` which throws
|
||||||
|
// an error in Node because `self` isn't defined.
|
||||||
|
//
|
||||||
|
// To work around this limitation we instead lookup the value of `self`
|
||||||
|
// through the `this` object, basically generating `this.self`.
|
||||||
|
//
|
||||||
|
// Unfortunately that's also hard to do! In ESM modes the top-level `this`
|
||||||
|
// object is undefined, meaning that we can't just generate a function that
|
||||||
|
// returns `this.self` as it'll throw "can't access field `self` of
|
||||||
|
// `undefined`" whenever ESMs are being used.
|
||||||
|
//
|
||||||
|
// So finally we reach the current implementation. According to
|
||||||
|
// StackOverflow you can access the global object via:
|
||||||
|
//
|
||||||
|
// const global = Function('return this')();
|
||||||
|
//
|
||||||
|
// I think that's because the manufactured function isn't in "strict" mode.
|
||||||
|
// It also turns out that non-strict functions will ignore `undefined`
|
||||||
|
// values for `this` when using the `apply` function. Add it all up, and you
|
||||||
|
// get the below code:
|
||||||
|
//
|
||||||
|
// * Manufacture a function
|
||||||
|
// * Call `apply` where we specify `this` but the function ignores it
|
||||||
|
// * Once we have `this`, use a structural getter to get the value of `self`
|
||||||
|
// * Last but not least, test whether `self` is defined or not.
|
||||||
|
//
|
||||||
|
// Whew!
|
||||||
|
let this = new_function("return this")
|
||||||
|
.apply(&JsValue::undefined(), &Array::new())
|
||||||
|
.unwrap();
|
||||||
|
assert!(this != JsValue::undefined());
|
||||||
|
This::from(this).self_() != JsValue::undefined()
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
#![doc(hidden)]
|
||||||
|
|
||||||
use std::cell::{RefCell, Cell};
|
use std::cell::{RefCell, Cell};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
@ -6,6 +8,10 @@ use console_error_panic_hook;
|
|||||||
use js_sys::{Array, Function};
|
use js_sys::{Array, Function};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
pub mod node;
|
||||||
|
pub mod browser;
|
||||||
|
pub mod detect;
|
||||||
|
|
||||||
/// Runtime test harness support instantiated in JS.
|
/// Runtime test harness support instantiated in JS.
|
||||||
///
|
///
|
||||||
/// The node.js entry script instantiates a `Context` here which is used to
|
/// The node.js entry script instantiates a `Context` here which is used to
|
||||||
@ -20,6 +26,15 @@ pub struct Context {
|
|||||||
current_log: RefCell<String>,
|
current_log: RefCell<String>,
|
||||||
current_error: RefCell<String>,
|
current_error: RefCell<String>,
|
||||||
ignore_this_test: Cell<bool>,
|
ignore_this_test: Cell<bool>,
|
||||||
|
formatter: Box<Formatter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Formatter {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@ -28,32 +43,26 @@ extern {
|
|||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn console_log(s: &str);
|
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`.
|
// General-purpose conversion into a `String`.
|
||||||
#[wasm_bindgen(js_name = String)]
|
#[wasm_bindgen(js_name = String)]
|
||||||
fn stringify(val: &JsValue) -> String;
|
fn stringify(val: &JsValue) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(module = "fs", version = "*")]
|
|
||||||
extern {
|
|
||||||
fn writeSync(fd: i32, data: &[u8]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log(args: &fmt::Arguments) {
|
pub fn log(args: &fmt::Arguments) {
|
||||||
console_log(&args.to_string());
|
console_log(&args.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
pub fn new() -> Context {
|
pub fn new() -> Context {
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
let formatter = match node::Node::new() {
|
||||||
|
Some(node) => Box::new(node) as Box<Formatter>,
|
||||||
|
None => Box::new(browser::Browser::new()),
|
||||||
|
};
|
||||||
Context {
|
Context {
|
||||||
filter: None,
|
filter: None,
|
||||||
current_test: RefCell::new(None),
|
current_test: RefCell::new(None),
|
||||||
@ -63,6 +72,7 @@ impl Context {
|
|||||||
current_log: RefCell::new(String::new()),
|
current_log: RefCell::new(String::new()),
|
||||||
current_error: RefCell::new(String::new()),
|
current_error: RefCell::new(String::new()),
|
||||||
ignore_this_test: Cell::new(false),
|
ignore_this_test: Cell::new(false),
|
||||||
|
formatter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,8 +105,8 @@ impl Context {
|
|||||||
args.push(&JsValue::from(self as *const Context as u32));
|
args.push(&JsValue::from(self as *const Context as u32));
|
||||||
|
|
||||||
let noun = if tests.len() == 1 { "test" } else { "tests" };
|
let noun = if tests.len() == 1 { "test" } else { "tests" };
|
||||||
console_log!("running {} {}", tests.len(), noun);
|
self.formatter.writeln(&format!("running {} {}", tests.len(), noun));
|
||||||
console_log!("");
|
self.formatter.writeln("");
|
||||||
|
|
||||||
for test in tests {
|
for test in tests {
|
||||||
self.ignore_this_test.set(false);
|
self.ignore_this_test.set(false);
|
||||||
@ -109,7 +119,7 @@ impl Context {
|
|||||||
self.log_success()
|
self.log_success()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => self.log_error(e.into()),
|
Err(e) => self.log_failure(e),
|
||||||
}
|
}
|
||||||
drop(self.current_test.borrow_mut().take());
|
drop(self.current_test.borrow_mut().take());
|
||||||
*self.current_log.borrow_mut() = String::new();
|
*self.current_log.borrow_mut() = String::new();
|
||||||
@ -123,22 +133,20 @@ impl Context {
|
|||||||
let mut current_test = self.current_test.borrow_mut();
|
let mut current_test = self.current_test.borrow_mut();
|
||||||
assert!(current_test.is_none());
|
assert!(current_test.is_none());
|
||||||
*current_test = Some(test.to_string());
|
*current_test = Some(test.to_string());
|
||||||
let data = format!("test {} ... ", test);
|
self.formatter.log_start(test);
|
||||||
writeSync(2, data.as_bytes());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_success(&self) {
|
fn log_success(&self) {
|
||||||
writeSync(2, b"ok\n");
|
self.formatter.log_success();
|
||||||
self.succeeded.set(self.succeeded.get() + 1);
|
self.succeeded.set(self.succeeded.get() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_ignore(&self) {
|
fn log_ignore(&self) {
|
||||||
writeSync(2, b"ignored\n");
|
self.formatter.log_ignored();
|
||||||
self.ignored.set(self.ignored.get() + 1);
|
self.ignored.set(self.ignored.get() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_error(&self, err: NodeError) {
|
fn log_failure(&self, err: JsValue) {
|
||||||
writeSync(2, b"FAILED\n");
|
|
||||||
let name = self.current_test.borrow().as_ref().unwrap().clone();
|
let name = self.current_test.borrow().as_ref().unwrap().clone();
|
||||||
let log = mem::replace(&mut *self.current_log.borrow_mut(), String::new());
|
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 error = mem::replace(&mut *self.current_error.borrow_mut(), String::new());
|
||||||
@ -154,25 +162,25 @@ impl Context {
|
|||||||
msg.push_str("\n");
|
msg.push_str("\n");
|
||||||
}
|
}
|
||||||
msg.push_str("JS exception that was thrown:\n");
|
msg.push_str("JS exception that was thrown:\n");
|
||||||
msg.push_str(&tab(&err.stack()));
|
msg.push_str(&tab(&self.formatter.log_failure(err)));
|
||||||
self.failures.borrow_mut().push((name, msg));
|
self.failures.borrow_mut().push((name, msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_results(&self) {
|
fn log_results(&self) {
|
||||||
let failures = self.failures.borrow();
|
let failures = self.failures.borrow();
|
||||||
if failures.len() > 0 {
|
if failures.len() > 0 {
|
||||||
console_log!("\nfailures:\n");
|
self.formatter.writeln("\nfailures:\n");
|
||||||
for (test, logs) in failures.iter() {
|
for (test, logs) in failures.iter() {
|
||||||
console_log!("---- {} output ----\n{}\n", test, tab(logs));
|
let msg = format!("---- {} output ----\n{}", test, tab(logs));
|
||||||
|
self.formatter.writeln(&msg);
|
||||||
}
|
}
|
||||||
console_log!("failures:\n");
|
self.formatter.writeln("failures:\n");
|
||||||
for (test, _) in failures.iter() {
|
for (test, _) in failures.iter() {
|
||||||
console_log!(" {}\n", test);
|
self.formatter.writeln(&format!(" {}", test));
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console_log!("");
|
|
||||||
}
|
}
|
||||||
console_log!(
|
self.formatter.writeln("");
|
||||||
|
self.formatter.writeln(&format!(
|
||||||
"test result: {}. \
|
"test result: {}. \
|
||||||
{} passed; \
|
{} passed; \
|
||||||
{} failed; \
|
{} failed; \
|
||||||
@ -181,7 +189,7 @@ impl Context {
|
|||||||
self.succeeded.get(),
|
self.succeeded.get(),
|
||||||
failures.len(),
|
failures.len(),
|
||||||
self.ignored.get(),
|
self.ignored.get(),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn console_log(&self, original: &Function, args: &Array) {
|
pub fn console_log(&self, original: &Function, args: &Array) {
|
60
crates/test/src/rt/node.rs
Normal file
60
crates/test/src/rt/node.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
//! Support for printing status information of a test suite in node.js
|
||||||
|
//!
|
||||||
|
//! This currently uses the same output as `libtest`, only reimplemented here
|
||||||
|
//! for node itself.
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use js_sys::eval;
|
||||||
|
|
||||||
|
pub struct Node {
|
||||||
|
fs: NodeFs,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern {
|
||||||
|
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;
|
||||||
|
#[wasm_bindgen(method, getter, js_class = "Error", structural)]
|
||||||
|
fn stack(this: &NodeError) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
pub fn new() -> Option<Node> {
|
||||||
|
if super::detect::is_browser() {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
|
||||||
|
let import = eval("require(\"fs\")").unwrap();
|
||||||
|
Some(Node { fs: import.into() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::Formatter for Node {
|
||||||
|
fn writeln(&self, line: &str) {
|
||||||
|
super::console_log(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_start(&self, name: &str) {
|
||||||
|
let data = format!("test {} ... ", name);
|
||||||
|
self.fs.write_sync(2, data.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
// TODO: should do a checked cast to `NodeError`
|
||||||
|
NodeError::from(err).stack()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user