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

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

240
crates/test/src/rt/mod.rs Normal file
View File

@ -0,0 +1,240 @@
#![doc(hidden)]
use std::cell::{RefCell, Cell};
use std::fmt;
use std::mem;
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
/// drive test execution.
#[wasm_bindgen]
pub struct Context {
filter: Option<String>,
current_test: RefCell<Option<String>>,
succeeded: Cell<usize>,
ignored: Cell<usize>,
failures: RefCell<Vec<(String, String)>>,
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]
extern {
#[wasm_bindgen(js_namespace = console, js_name = log)]
#[doc(hidden)]
pub fn console_log(s: &str);
// General-purpose conversion into a `String`.
#[wasm_bindgen(js_name = String)]
fn stringify(val: &JsValue) -> String;
}
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),
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,
}
}
/// 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<JsValue>) {
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<JsValue>) -> 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" };
self.formatter.writeln(&format!("running {} {}", tests.len(), noun));
self.formatter.writeln("");
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_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
}
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);
}
fn log_success(&self) {
self.formatter.log_success();
self.succeeded.set(self.succeeded.get() + 1);
}
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");
}
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(&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 {
self.formatter.writeln("\nfailures:\n");
for (test, logs) in failures.iter() {
let msg = format!("---- {} output ----\n{}", test, tab(logs));
self.formatter.writeln(&msg);
}
self.formatter.writeln("failures:\n");
for (test, _) in failures.iter() {
self.formatter.writeln(&format!(" {}", test));
}
}
self.formatter.writeln("");
self.formatter.writeln(&format!(
"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<String>) {
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;
}

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