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:
Alex Crichton 2018-07-24 11:32:18 -07:00
parent 0770f830e7
commit 8fc40e4c0f
5 changed files with 271 additions and 30 deletions

View File

@ -20,6 +20,6 @@ macro_rules! console_log {
)
}
#[path = "rt.rs"]
#[path = "rt/mod.rs"]
#[doc(hidden)]
pub mod __rt;

View 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)
}
}

View 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()
}

View File

@ -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) {

View 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()
}
}