mirror of
https://github.com/rustwasm/wasm-bindgen.git
synced 2024-12-26 19:45:54 +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)]
|
||||
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::fmt;
|
||||
use std::mem;
|
||||
@ -6,6 +8,10 @@ use console_error_panic_hook;
|
||||
use js_sys::{Array, Function};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub mod node;
|
||||
pub mod browser;
|
||||
pub mod detect;
|
||||
|
||||
/// Runtime test harness support instantiated in JS.
|
||||
///
|
||||
/// 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_error: RefCell<String>,
|
||||
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]
|
||||
@ -28,32 +43,26 @@ extern {
|
||||
#[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();
|
||||
|
||||
let formatter = match node::Node::new() {
|
||||
Some(node) => Box::new(node) as Box<Formatter>,
|
||||
None => Box::new(browser::Browser::new()),
|
||||
};
|
||||
Context {
|
||||
filter: None,
|
||||
current_test: RefCell::new(None),
|
||||
@ -63,6 +72,7 @@ impl Context {
|
||||
current_log: RefCell::new(String::new()),
|
||||
current_error: RefCell::new(String::new()),
|
||||
ignore_this_test: Cell::new(false),
|
||||
formatter,
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,8 +105,8 @@ impl Context {
|
||||
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!("");
|
||||
self.formatter.writeln(&format!("running {} {}", tests.len(), noun));
|
||||
self.formatter.writeln("");
|
||||
|
||||
for test in tests {
|
||||
self.ignore_this_test.set(false);
|
||||
@ -109,7 +119,7 @@ impl Context {
|
||||
self.log_success()
|
||||
}
|
||||
}
|
||||
Err(e) => self.log_error(e.into()),
|
||||
Err(e) => self.log_failure(e),
|
||||
}
|
||||
drop(self.current_test.borrow_mut().take());
|
||||
*self.current_log.borrow_mut() = String::new();
|
||||
@ -123,22 +133,20 @@ impl Context {
|
||||
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());
|
||||
self.formatter.log_start(test);
|
||||
}
|
||||
|
||||
fn log_success(&self) {
|
||||
writeSync(2, b"ok\n");
|
||||
self.formatter.log_success();
|
||||
self.succeeded.set(self.succeeded.get() + 1);
|
||||
}
|
||||
|
||||
fn log_ignore(&self) {
|
||||
writeSync(2, b"ignored\n");
|
||||
self.formatter.log_ignored();
|
||||
self.ignored.set(self.ignored.get() + 1);
|
||||
}
|
||||
|
||||
fn log_error(&self, err: NodeError) {
|
||||
writeSync(2, b"FAILED\n");
|
||||
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());
|
||||
@ -154,25 +162,25 @@ impl Context {
|
||||
msg.push_str("\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));
|
||||
}
|
||||
|
||||
fn log_results(&self) {
|
||||
let failures = self.failures.borrow();
|
||||
if failures.len() > 0 {
|
||||
console_log!("\nfailures:\n");
|
||||
self.formatter.writeln("\nfailures:\n");
|
||||
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() {
|
||||
console_log!(" {}\n", test);
|
||||
self.formatter.writeln(&format!(" {}", test));
|
||||
}
|
||||
} else {
|
||||
console_log!("");
|
||||
}
|
||||
console_log!(
|
||||
self.formatter.writeln("");
|
||||
self.formatter.writeln(&format!(
|
||||
"test result: {}. \
|
||||
{} passed; \
|
||||
{} failed; \
|
||||
@ -181,7 +189,7 @@ impl Context {
|
||||
self.succeeded.get(),
|
||||
failures.len(),
|
||||
self.ignored.get(),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
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