use alloc::borrow::ToOwned;
use alloc::boxed::Box;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::cell::{Cell, RefCell};
use core::fmt::{self, Display};
use core::future::Future;
use core::pin::Pin;
use core::task::{self, Poll};
use js_sys::{Array, Function, Promise};
pub use wasm_bindgen;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
const CONCURRENCY: usize = 1;
pub mod browser;
pub mod detect;
pub mod node;
mod scoped_tls;
pub mod worker;
#[wasm_bindgen(js_name = WasmBindgenTestContext)]
pub struct Context {
state: Rc<State>,
}
struct State {
include_ignored: Cell<bool>,
succeeded_count: Cell<usize>,
filtered_count: Cell<usize>,
ignored_count: Cell<usize>,
failures: RefCell<Vec<(Test, Failure)>>,
remaining: RefCell<Vec<Test>>,
running: RefCell<Vec<Test>>,
formatter: Box<dyn Formatter>,
timer: Option<Timer>,
}
enum Failure {
Error(JsValue),
ShouldPanic,
ShouldPanicExpected,
}
struct Test {
name: String,
future: Pin<Box<dyn Future<Output = Result<(), JsValue>>>>,
output: Rc<RefCell<Output>>,
should_panic: Option<Option<&'static str>>,
}
#[derive(Default)]
struct Output {
debug: String,
log: String,
info: String,
warn: String,
error: String,
panic: String,
should_panic: bool,
}
enum TestResult {
Ok,
Err(JsValue),
Ignored(Option<String>),
}
impl From<Result<(), JsValue>> for TestResult {
fn from(value: Result<(), JsValue>) -> Self {
match value {
Ok(()) => Self::Ok,
Err(err) => Self::Err(err),
}
}
}
impl Display for TestResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TestResult::Ok => write!(f, "ok"),
TestResult::Err(_) => write!(f, "FAIL"),
TestResult::Ignored(None) => write!(f, "ignored"),
TestResult::Ignored(Some(reason)) => write!(f, "ignored, {}", reason),
}
}
}
trait Formatter {
fn writeln(&self, line: &str);
fn log_test(&self, name: &str, result: &TestResult);
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_namespace = console, js_name = error)]
#[doc(hidden)]
pub fn js_console_error(s: &str);
#[wasm_bindgen(js_name = String)]
fn stringify(val: &JsValue) -> String;
type Global;
#[wasm_bindgen(method, getter)]
fn performance(this: &Global) -> JsValue;
type Performance;
#[wasm_bindgen(method)]
fn now(this: &Performance) -> f64;
}
pub fn console_log(args: &fmt::Arguments) {
js_console_log(&args.to_string());
}
pub fn console_error(args: &fmt::Arguments) {
js_console_error(&args.to_string());
}
#[wasm_bindgen(js_class = WasmBindgenTestContext)]
impl Context {
#[wasm_bindgen(constructor)]
pub fn new() -> Context {
fn panic_handling(mut message: String) {
let should_panic = CURRENT_OUTPUT.with(|output| {
let mut output = output.borrow_mut();
output.panic.push_str(&message);
output.should_panic
});
if !should_panic {
#[wasm_bindgen]
extern "C" {
type Error;
#[wasm_bindgen(constructor)]
fn new() -> Error;
#[wasm_bindgen(method, getter)]
fn stack(error: &Error) -> String;
}
message.push_str("\n\nStack:\n\n");
let e = Error::new();
let stack = e.stack();
message.push_str(&stack);
message.push_str("\n\n");
js_console_error(&message);
}
}
#[cfg(feature = "std")]
static SET_HOOK: std::sync::Once = std::sync::Once::new();
#[cfg(feature = "std")]
SET_HOOK.call_once(|| {
std::panic::set_hook(Box::new(|panic_info| {
panic_handling(panic_info.to_string());
}));
});
#[cfg(all(
not(feature = "std"),
target_arch = "wasm32",
any(target_os = "unknown", target_os = "none")
))]
#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo<'_>) -> ! {
panic_handling(panic_info.to_string());
core::arch::wasm32::unreachable();
}
let formatter = match detect::detect() {
detect::Runtime::Browser => Box::new(browser::Browser::new()) as Box<dyn Formatter>,
detect::Runtime::Node => Box::new(node::Node::new()) as Box<dyn Formatter>,
detect::Runtime::Worker => Box::new(worker::Worker::new()) as Box<dyn Formatter>,
};
let timer = Timer::new();
Context {
state: Rc::new(State {
include_ignored: Default::default(),
failures: Default::default(),
succeeded_count: Default::default(),
filtered_count: Default::default(),
ignored_count: Default::default(),
remaining: Default::default(),
running: Default::default(),
formatter,
timer,
}),
}
}
pub fn include_ignored(&mut self, include_ignored: bool) {
self.state.include_ignored.set(include_ignored);
}
pub fn filtered_count(&mut self, filtered: usize) {
self.state.filtered_count.set(filtered);
}
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));
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 state = self.state.clone();
future_to_promise(async {
let passed = ExecuteTests(state).await;
Ok(JsValue::from(passed))
})
}
}
crate::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(' ');
}
dst.push_str(&stringify(&val));
});
dst.push('\n');
});
}
pub trait Termination {
fn into_js_result(self) -> Result<(), JsValue>;
}
impl Termination for () {
fn into_js_result(self) -> Result<(), JsValue> {
Ok(())
}
}
impl<E: core::fmt::Debug> Termination for Result<(), E> {
fn into_js_result(self) -> Result<(), JsValue> {
self.map_err(|e| JsError::new(&format!("{:?}", e)).into())
}
}
impl Context {
pub fn execute_sync<T: Termination>(
&self,
name: &str,
f: impl 'static + FnOnce() -> T,
should_panic: Option<Option<&'static str>>,
ignore: Option<Option<&'static str>>,
) {
self.execute(name, async { f().into_js_result() }, should_panic, ignore);
}
pub fn execute_async<F>(
&self,
name: &str,
f: impl FnOnce() -> F + 'static,
should_panic: Option<Option<&'static str>>,
ignore: Option<Option<&'static str>>,
) where
F: Future + 'static,
F::Output: Termination,
{
self.execute(
name,
async { f().await.into_js_result() },
should_panic,
ignore,
)
}
fn execute(
&self,
name: &str,
test: impl Future<Output = Result<(), JsValue>> + 'static,
should_panic: Option<Option<&'static str>>,
ignore: Option<Option<&'static str>>,
) {
let name = name.split_once("::").unwrap().1;
if let Some(ignore) = ignore {
if !self.state.include_ignored.get() {
self.state
.formatter
.log_test(name, &TestResult::Ignored(ignore.map(str::to_owned)));
let ignored = self.state.ignored_count.get();
self.state.ignored_count.set(ignored + 1);
return;
}
}
let output = Output {
should_panic: should_panic.is_some(),
..Default::default()
};
let output = Rc::new(RefCell::new(output));
let future = TestFuture {
output: output.clone(),
test,
};
self.state.remaining.borrow_mut().push(Test {
name: name.to_string(),
future: Pin::from(Box::new(future)),
output,
should_panic,
});
}
}
struct ExecuteTests(Rc<State>);
impl Future for ExecuteTests {
type Output = bool;
fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> Poll<bool> {
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.as_mut().poll(cx) {
Poll::Ready(result) => result,
Poll::Pending => continue,
};
let test = running.remove(i);
self.0.log_test_result(test, result.into());
}
while running.len() < CONCURRENCY {
let mut test = match remaining.pop() {
Some(test) => test,
None => break,
};
let result = match test.future.as_mut().poll(cx) {
Poll::Ready(result) => result,
Poll::Pending => {
running.push(test);
continue;
}
};
self.0.log_test_result(test, result.into());
}
if !running.is_empty() {
return Poll::Pending;
}
assert_eq!(remaining.len(), 0);
self.0.print_results();
let all_passed = self.0.failures.borrow().is_empty();
Poll::Ready(all_passed)
}
}
impl State {
fn log_test_result(&self, test: Test, result: TestResult) {
if let Some(should_panic) = test.should_panic {
if let TestResult::Err(_e) = result {
if let Some(expected) = should_panic {
if !test.output.borrow().panic.contains(expected) {
self.formatter
.log_test(&test.name, &TestResult::Err(JsValue::NULL));
self.failures
.borrow_mut()
.push((test, Failure::ShouldPanicExpected));
return;
}
}
self.formatter.log_test(&test.name, &TestResult::Ok);
self.succeeded_count.set(self.succeeded_count.get() + 1);
} else {
self.formatter
.log_test(&test.name, &TestResult::Err(JsValue::NULL));
self.failures
.borrow_mut()
.push((test, Failure::ShouldPanic));
}
} else {
self.formatter.log_test(&test.name, &result);
match result {
TestResult::Ok => self.succeeded_count.set(self.succeeded_count.get() + 1),
TestResult::Err(e) => self.failures.borrow_mut().push((test, Failure::Error(e))),
_ => (),
}
}
}
fn print_results(&self) {
let failures = self.failures.borrow();
if !failures.is_empty() {
self.formatter.writeln("\nfailures:\n");
for (test, failure) in failures.iter() {
self.print_failure(test, failure);
}
self.formatter.writeln("failures:\n");
for (test, _) in failures.iter() {
self.formatter.writeln(&format!(" {}", test.name));
}
}
let finished_in = if let Some(timer) = &self.timer {
format!("; finished in {:.2?}s", timer.elapsed())
} else {
String::new()
};
self.formatter.writeln("");
self.formatter.writeln(&format!(
"test result: {}. \
{} passed; \
{} failed; \
{} ignored; \
{} filtered out\
{}\n",
if failures.is_empty() { "ok" } else { "FAILED" },
self.succeeded_count.get(),
failures.len(),
self.ignored_count.get(),
self.filtered_count.get(),
finished_in,
));
}
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, failure: &Failure) {
let mut logs = String::new();
let output = test.output.borrow();
match failure {
Failure::ShouldPanic => {
logs.push_str(&format!(
"note: {} did not panic as expected\n\n",
test.name
));
}
Failure::ShouldPanicExpected => {
logs.push_str("note: panic did not contain expected string\n");
logs.push_str(&format!(" panic message: `\"{}\"`,\n", output.panic));
logs.push_str(&format!(
" expected substring: `\"{}\"`\n\n",
test.should_panic.unwrap().unwrap()
));
}
_ => (),
}
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);
if let Failure::Error(error) = failure {
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 dyn FnMut()) -> Result<(), JsValue>;
}
impl<F: Future<Output = Result<(), JsValue>>> Future for TestFuture<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> Poll<Self::Output> {
let output = self.output.clone();
let test = unsafe { Pin::map_unchecked_mut(self, |me| &mut me.test) };
let mut future_output = None;
let result = CURRENT_OUTPUT.set(&output, || {
let mut test = Some(test);
__wbg_test_invoke(&mut || {
let test = test.take().unwrap_throw();
future_output = Some(test.poll(cx))
})
});
match (result, future_output) {
(_, Some(Poll::Ready(result))) => Poll::Ready(result),
(_, Some(Poll::Pending)) => Poll::Pending,
(Err(e), _) => Poll::Ready(Err(e)),
(Ok(_), None) => wasm_bindgen::throw_str("invalid poll state"),
}
}
}
fn tab(s: &str) -> String {
let mut result = String::new();
for line in s.lines() {
result.push_str(" ");
result.push_str(line);
result.push('\n');
}
result
}
struct Timer {
performance: Performance,
started: f64,
}
impl Timer {
fn new() -> Option<Self> {
let global: Global = js_sys::global().unchecked_into();
let performance = global.performance();
(!performance.is_undefined()).then(|| {
let performance: Performance = performance.unchecked_into();
let started = performance.now();
Self {
performance,
started,
}
})
}
fn elapsed(&self) -> f64 {
(self.performance.now() - self.started) / 1000.
}
}