use std::cell::{Cell, RefCell};
use std::fmt;
use std::rc::Rc;
use console_error_panic_hook;
use futures::future;
use futures::prelude::*;
use js_sys::{Array, Function, Promise};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
const CONCURRENCY: usize = 1;
pub mod browser;
pub mod detect;
pub mod node;
#[wasm_bindgen]
pub struct Context {
state: Rc<State>,
}
struct State {
filter: RefCell<Option<String>>,
succeeded: Cell<usize>,
ignored: Cell<usize>,
failures: RefCell<Vec<(Test, JsValue)>>,
remaining: RefCell<Vec<Test>>,
running: RefCell<Vec<Test>>,
formatter: Box<Formatter>,
}
struct Test {
name: String,
future: Box<Future<Item = (), Error = JsValue>>,
output: Rc<RefCell<Output>>,
}
#[derive(Default)]
struct Output {
debug: String,
log: String,
info: String,
warn: String,
error: String,
}
trait Formatter {
fn writeln(&self, line: &str);
fn log_test(&self, name: &str, result: &Result<(), JsValue>);
fn stringify_error(&self, val: &JsValue) -> String;
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console, js_name = log)]
#[doc(hidden)]
pub fn js_console_log(s: &str);
#[wasm_bindgen(js_name = String)]
fn stringify(val: &JsValue) -> String;
}
pub fn log(args: &fmt::Arguments) {
js_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 {
state: Rc::new(State {
filter: Default::default(),
failures: Default::default(),
ignored: Default::default(),
remaining: Default::default(),
running: Default::default(),
succeeded: Default::default(),
formatter,
}),
}
}
pub fn args(&mut self, args: Vec<JsValue>) {
let mut filter = self.state.filter.borrow_mut();
for arg in args {
let arg = arg.as_string().unwrap();
if arg.starts_with("-") {
panic!("flag {} not supported", arg);
} else if filter.is_some() {
panic!("more than one filter argument cannot be passed");
}
*filter = Some(arg);
}
}
pub fn run(&self, tests: Vec<JsValue>) -> Promise {
let noun = if tests.len() == 1 { "test" } else { "tests" };
self.state
.formatter
.writeln(&format!("running {} {}", tests.len(), noun));
self.state.formatter.writeln("");
let cx_arg = (self as *const Context as u32).into();
for test in tests {
match Function::from(test).call1(&JsValue::null(), &cx_arg) {
Ok(_) => {}
Err(e) => {
panic!(
"exception thrown while creating a test: {}",
self.state.formatter.stringify_error(&e)
);
}
}
}
let future = ExecuteTests(self.state.clone())
.map(JsValue::from)
.map_err(|e| match e {});
future_to_promise(future)
}
}
scoped_thread_local!(static CURRENT_OUTPUT: RefCell<Output>);
#[wasm_bindgen]
pub fn __wbgtest_console_log(args: &Array) {
record(args, |output| &mut output.log)
}
#[wasm_bindgen]
pub fn __wbgtest_console_debug(args: &Array) {
record(args, |output| &mut output.debug)
}
#[wasm_bindgen]
pub fn __wbgtest_console_info(args: &Array) {
record(args, |output| &mut output.info)
}
#[wasm_bindgen]
pub fn __wbgtest_console_warn(args: &Array) {
record(args, |output| &mut output.warn)
}
#[wasm_bindgen]
pub fn __wbgtest_console_error(args: &Array) {
record(args, |output| &mut output.error)
}
fn record(args: &Array, dst: impl FnOnce(&mut Output) -> &mut String) {
if !CURRENT_OUTPUT.is_set() {
return;
}
CURRENT_OUTPUT.with(|output| {
let mut out = output.borrow_mut();
let dst = dst(&mut out);
args.for_each(&mut |val, idx, _array| {
if idx != 0 {
dst.push_str(" ");
}
dst.push_str(&stringify(&val));
});
dst.push_str("\n");
});
}
impl Context {
pub fn execute_sync(&self, name: &str, f: impl FnOnce() + 'static) {
self.execute(name, future::lazy(|| Ok(f())));
}
pub fn execute_async<F>(&self, name: &str, f: impl FnOnce() -> F + 'static)
where
F: Future<Item = (), Error = JsValue> + 'static,
{
self.execute(name, future::lazy(f))
}
fn execute(&self, name: &str, test: impl Future<Item = (), Error = JsValue> + 'static) {
let filter = self.state.filter.borrow();
if let Some(filter) = &*filter {
if !name.contains(filter) {
let ignored = self.state.ignored.get();
self.state.ignored.set(ignored + 1);
return;
}
}
let output = Rc::new(RefCell::new(Output::default()));
let future = TestFuture {
output: output.clone(),
test,
};
self.state.remaining.borrow_mut().push(Test {
name: name.to_string(),
future: Box::new(future),
output,
});
}
}
struct ExecuteTests(Rc<State>);
enum Never {}
impl Future for ExecuteTests {
type Item = bool;
type Error = Never;
fn poll(&mut self) -> Poll<bool, Never> {
let mut running = self.0.running.borrow_mut();
let mut remaining = self.0.remaining.borrow_mut();
for i in (0..running.len()).rev() {
let result = match running[i].future.poll() {
Ok(Async::Ready(_jsavl)) => Ok(()),
Ok(Async::NotReady) => continue,
Err(e) => Err(e),
};
let test = running.remove(i);
self.0.log_test_result(test, result);
}
while running.len() < CONCURRENCY {
let mut test = match remaining.pop() {
Some(test) => test,
None => break,
};
let result = match test.future.poll() {
Ok(Async::Ready(())) => Ok(()),
Ok(Async::NotReady) => {
running.push(test);
continue;
}
Err(e) => Err(e),
};
self.0.log_test_result(test, result);
}
if running.len() != 0 {
return Ok(Async::NotReady);
}
assert_eq!(remaining.len(), 0);
self.0.print_results();
let all_passed = self.0.failures.borrow().len() == 0;
Ok(Async::Ready(all_passed))
}
}
impl State {
fn log_test_result(&self, test: Test, result: Result<(), JsValue>) {
self.formatter.log_test(&test.name, &result);
match result {
Ok(()) => self.succeeded.set(self.succeeded.get() + 1),
Err(e) => self.failures.borrow_mut().push((test, e)),
}
}
fn print_results(&self) {
let failures = self.failures.borrow();
if failures.len() > 0 {
self.formatter.writeln("\nfailures:\n");
for (test, error) in failures.iter() {
self.print_failure(test, error);
}
self.formatter.writeln("failures:\n");
for (test, _) in failures.iter() {
self.formatter.writeln(&format!(" {}", test.name));
}
}
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(),
));
}
fn accumulate_console_output(&self, logs: &mut String, which: &str, output: &str) {
if output.is_empty() {
return;
}
logs.push_str(which);
logs.push_str(" output:\n");
logs.push_str(&tab(output));
logs.push('\n');
}
fn print_failure(&self, test: &Test, error: &JsValue) {
let mut logs = String::new();
let output = test.output.borrow();
self.accumulate_console_output(&mut logs, "debug", &output.debug);
self.accumulate_console_output(&mut logs, "log", &output.log);
self.accumulate_console_output(&mut logs, "info", &output.info);
self.accumulate_console_output(&mut logs, "warn", &output.warn);
self.accumulate_console_output(&mut logs, "error", &output.error);
logs.push_str("JS exception that was thrown:\n");
let error_string = self.formatter.stringify_error(error);
logs.push_str(&tab(&error_string));
let msg = format!("---- {} output ----\n{}", test.name, tab(&logs));
self.formatter.writeln(&msg);
}
}
struct TestFuture<F> {
output: Rc<RefCell<Output>>,
test: F,
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(catch)]
fn __wbg_test_invoke(f: &mut FnMut()) -> Result<(), JsValue>;
}
impl<F: Future<Error = JsValue>> Future for TestFuture<F> {
type Item = F::Item;
type Error = F::Error;
fn poll(&mut self) -> Poll<F::Item, F::Error> {
let test = &mut self.test;
let mut future_output = None;
CURRENT_OUTPUT.set(&self.output, || {
__wbg_test_invoke(&mut || future_output = Some(test.poll()))
})?;
future_output.unwrap()
}
}
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;
}