wasm_bindgen_test/rt/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791
//! Internal-only runtime module used for the `wasm_bindgen_test` crate.
//!
//! No API contained in this module will respect semver, these should all be
//! considered private APIs.
// # Architecture of `wasm_bindgen_test`
//
// This module can seem a bit funky, but it's intended to be the runtime support
// of the `#[wasm_bindgen_test]` macro and be amenable to executing Wasm test
// suites. The general idea is that for a Wasm test binary there will be a set
// of functions tagged `#[wasm_bindgen_test]`. It's the job of the runtime
// support to execute all of these functions, collecting and collating the
// results.
//
// This runtime support works in tandem with the `wasm-bindgen-test-runner`
// binary as part of the `wasm-bindgen-cli` package.
//
// ## High Level Overview
//
// Here's a rough and (semi) high level overview of what happens when this crate
// runs.
//
// * First, the user runs `cargo test --target wasm32-unknown-unknown`
//
// * Cargo then compiles all the test suites (aka `tests/*.rs`) as Wasm binaries
// (the `bin` crate type). These binaries all have entry points that are
// `main` functions, but it's actually not used. The binaries are also
// compiled with `--test`, which means they're linked to the standard `test`
// crate, but this crate doesn't work on Wasm and so we bypass it entirely.
//
// * Instead of using `#[test]`, which doesn't work, users wrote tests with
// `#[wasm_bindgen_test]`. This macro expands to a bunch of `#[no_mangle]`
// functions with known names (currently named `__wbg_test_*`).
//
// * Next up, Cargo was configured via its test runner support to execute the
// `wasm-bindgen-test-runner` binary. Instead of what Cargo normally does,
// executing `target/wasm32-unknown-unknown/debug/deps/foo-xxxxx.wasm` (which
// will fail as we can't actually execute was binaries), Cargo will execute
// `wasm-bindgen-test-runner target/.../foo-xxxxx.wasm`.
//
// * The `wasm-bindgen-test-runner` binary takes over. It runs `wasm-bindgen`
// over the binary, generating JS bindings and such. It also figures out if
// we're running in node.js or a browser.
//
// * The `wasm-bindgen-test-runner` binary generates a JS entry point. This
// entry point creates a `Context` below. The runner binary also parses the
// Wasm file and finds all functions that are named `__wbg_test_*`. The
// generate file gathers up all these functions into an array and then passes
// them to `Context` below. Note that these functions are passed as *JS
// values*.
//
// * Somehow, the runner then executes the JS file. This may be with node.js, it
// may serve up files in a server and wait for the user, or it serves up files
// in a server and starts headless testing.
//
// * Testing starts, it loads all the modules using either ES imports or Node
// `require` statements. Everything is loaded in JS now.
//
// * A `Context` is created. The `Context` is forwarded the CLI arguments of the
// original `wasm-bindgen-test-runner` in an environment specific fashion.
// This is used for test filters today.
//
// * The `Context::run` function is called. Again, the generated JS has gathered
// all Wasm tests to be executed into a list, and it's passed in here.
//
// * Next, `Context::run` returns a `Promise` representing the eventual
// execution of all the tests. The Rust `Future` that's returned will work
// with the tests to ensure that everything's executed by the time the
// `Promise` resolves.
//
// * When a test executes, it's executing an entry point generated by
// `#[wasm_bindgen_test]`. The test informs the `Context` of its name and
// other metadata, and then `Context::execute_*` function creates a future
// representing the execution of the test. This feeds back into the future
// returned by `Context::run` to finish the test suite.
//
// * Finally, after all tests are run, the `Context`'s future resolves, prints
// out all the result, and finishes in JS.
//
// ## Other various notes
//
// Phew, that was a lot! Some other various bits and pieces you may want to be
// aware of are throughout the code. These include things like how printing
// results is different in node vs a browser, or how we even detect if we're in
// node or a browser.
//
// Overall this is all somewhat in flux as it's pretty new, and feedback is
// always of course welcome!
use js_sys::{Array, Function, Promise};
use std::cell::{Cell, RefCell};
use std::fmt::{self, Display};
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::Once;
use std::task::{self, Poll};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
// Maximum number of tests to execute concurrently. Eventually this should be a
// configuration option specified at runtime or at compile time rather than
// baked in here.
//
// Currently the default is 1 because the DOM has a lot of shared state, and
// conccurrently doing things by default would likely end up in a bad situation.
const CONCURRENCY: usize = 1;
pub mod browser;
pub mod detect;
pub mod node;
pub mod worker;
/// 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(js_name = WasmBindgenTestContext)]
pub struct Context {
state: Rc<State>,
}
struct State {
/// An optional filter used to restrict which tests are actually executed
/// and which are ignored. This is passed via the `args` function which
/// comes from the command line of `wasm-bindgen-test-runner`. Currently
/// this is the only "CLI option"
filter: RefCell<Option<String>>,
/// Include ignored tests.
include_ignored: Cell<bool>,
/// Tests to skip.
skip: RefCell<Vec<String>>,
/// Counter of the number of tests that have succeeded.
succeeded: Cell<usize>,
/// Counter of the number of tests that have been filtered
filtered: Cell<usize>,
/// Counter of the number of tests that have been ignored
ignored: Cell<usize>,
/// A list of all tests which have failed.
///
/// Each test listed here is paired with a `JsValue` that represents the
/// exception thrown which caused the test to fail.
failures: RefCell<Vec<(Test, Failure)>>,
/// Remaining tests to execute, when empty we're just waiting on the
/// `Running` tests to finish.
remaining: RefCell<Vec<Test>>,
/// List of currently executing tests. These tests all involve some level
/// of asynchronous work, so they're sitting on the running list.
running: RefCell<Vec<Test>>,
/// How to actually format output, either node.js or browser-specific
/// implementation.
formatter: Box<dyn Formatter>,
}
/// Failure reasons.
enum Failure {
/// Normal failing test.
Error(JsValue),
/// A test that `should_panic` but didn't.
ShouldPanic,
/// A test that `should_panic` with a specific message,
/// but panicked with a different message.
ShouldPanicExpected,
}
/// Representation of one test that needs to be executed.
///
/// Tests are all represented as futures, and tests perform no work until their
/// future is polled.
struct Test {
name: String,
future: Pin<Box<dyn Future<Output = Result<(), JsValue>>>>,
output: Rc<RefCell<Output>>,
should_panic: Option<Option<&'static str>>,
}
/// Captured output of each test.
#[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 {
/// Writes a line of output, typically status information.
fn writeln(&self, line: &str);
/// Log the result of a test, either passing or failing.
fn log_test(&self, name: &str, result: &TestResult);
/// Convert a thrown value into a string, using platform-specific apis
/// perhaps to turn the error into a string.
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);
// General-purpose conversion into a `String`.
#[wasm_bindgen(js_name = String)]
fn stringify(val: &JsValue) -> String;
}
/// Internal implementation detail of the `console_log!` macro.
pub fn console_log(args: &fmt::Arguments) {
js_console_log(&args.to_string());
}
/// Internal implementation detail of the `console_error!` macro.
pub fn console_error(args: &fmt::Arguments) {
js_console_error(&args.to_string());
}
#[wasm_bindgen(js_class = WasmBindgenTestContext)]
impl Context {
/// Creates a new context ready to run tests.
///
/// A `Context` is the main structure through which test execution is
/// coordinated, and this will collect output and results for all executed
/// tests.
#[wasm_bindgen(constructor)]
pub fn new() -> Context {
static SET_HOOK: Once = Once::new();
SET_HOOK.call_once(|| {
std::panic::set_hook(Box::new(|panic_info| {
let should_panic = CURRENT_OUTPUT.with(|output| {
let mut output = output.borrow_mut();
output.panic.push_str(&panic_info.to_string());
output.should_panic
});
if !should_panic {
console_error_panic_hook::hook(panic_info);
}
}));
});
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>,
};
Context {
state: Rc::new(State {
filter: Default::default(),
include_ignored: Default::default(),
skip: Default::default(),
failures: Default::default(),
filtered: Default::default(),
ignored: Default::default(),
remaining: Default::default(),
running: Default::default(),
succeeded: Default::default(),
formatter,
}),
}
}
/// Inform this context about runtime arguments passed to the test
/// harness.
pub fn args(&mut self, args: Vec<JsValue>) {
let mut filter = self.state.filter.borrow_mut();
let mut skip = self.state.skip.borrow_mut();
let mut args = args.into_iter();
while let Some(arg) = args.next() {
let arg = arg.as_string().unwrap();
if arg == "--include-ignored" {
self.state.include_ignored.set(true);
} else if arg == "--skip" {
skip.push(
args.next()
.expect("Argument to option 'skip' missing")
.as_string()
.unwrap(),
);
} else if let Some(arg) = arg.strip_prefix("--skip=") {
skip.push(arg.to_owned())
} else if arg.starts_with('-') {
panic!("flag {} not supported", arg);
} else if filter.is_some() {
panic!("more than one filter argument cannot be passed");
} else {
*filter = Some(arg);
}
}
}
/// Executes a list of tests, returning a promise representing their
/// eventual completion.
///
/// 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.
///
/// The promise returned resolves to either `true` if all tests passed or
/// `false` if at least one test failed.
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("");
// Execute all our test functions through their Wasm shims (unclear how
// to pass native function pointers around here). Each test will
// execute one of the `execute_*` tests below which will push a
// future onto our `remaining` list, which we'll process later.
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)
);
}
}
}
// Now that we've collected all our tests we wrap everything up in a
// future to actually do all the processing, and pass it out to JS as a
// `Promise`.
let state = self.state.clone();
future_to_promise(async {
let passed = ExecuteTests(state).await;
Ok(JsValue::from(passed))
})
}
}
scoped_tls::scoped_thread_local!(static CURRENT_OUTPUT: RefCell<Output>);
/// Handler for `console.log` invocations.
///
/// If a test is currently running it takes the `args` array and stringifies
/// it and appends it to the current output of the test. Otherwise it passes
/// the arguments to the original `console.log` function, psased as
/// `original`.
//
// TODO: how worth is it to actually capture the output here? Due to the nature
// of futures/js we can't guarantee that all output is captured because JS code
// could just be executing in the void and we wouldn't know which test to
// attach it to. The main `test` crate in the rust repo also has issues about
// how not all output is captured, causing some inconsistencies sometimes.
#[wasm_bindgen]
pub fn __wbgtest_console_log(args: &Array) {
record(args, |output| &mut output.log)
}
/// Handler for `console.debug` invocations. See above.
#[wasm_bindgen]
pub fn __wbgtest_console_debug(args: &Array) {
record(args, |output| &mut output.debug)
}
/// Handler for `console.info` invocations. See above.
#[wasm_bindgen]
pub fn __wbgtest_console_info(args: &Array) {
record(args, |output| &mut output.info)
}
/// Handler for `console.warn` invocations. See above.
#[wasm_bindgen]
pub fn __wbgtest_console_warn(args: &Array) {
record(args, |output| &mut output.warn)
}
/// Handler for `console.error` invocations. See above.
#[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');
});
}
/// Similar to [`std::process::Termination`], but for wasm-bindgen tests.
pub trait Termination {
/// Convert this into a JS result.
fn into_js_result(self) -> Result<(), JsValue>;
}
impl Termination for () {
fn into_js_result(self) -> Result<(), JsValue> {
Ok(())
}
}
impl<E: std::fmt::Debug> Termination for Result<(), E> {
fn into_js_result(self) -> Result<(), JsValue> {
self.map_err(|e| JsError::new(&format!("{:?}", e)).into())
}
}
impl Context {
/// Entry point for a synchronous test in wasm. The `#[wasm_bindgen_test]`
/// macro generates invocations of this method.
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);
}
/// Entry point for an asynchronous in wasm. The
/// `#[wasm_bindgen_test(async)]` macro generates invocations of this
/// method.
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>>,
) {
// If our test is filtered out, record that it was filtered and move
// on, nothing to do here.
let filter = self.state.filter.borrow();
if let Some(filter) = &*filter {
if !name.contains(filter) {
let filtered = self.state.filtered.get();
self.state.filtered.set(filtered + 1);
return;
}
}
for skip in &*self.state.skip.borrow() {
if name.contains(skip) {
let filtered = self.state.filtered.get();
self.state.filtered.set(filtered + 1);
return;
}
}
if !self.state.include_ignored.get() {
if let Some(ignore) = ignore {
self.state
.formatter
.log_test(name, &TestResult::Ignored(ignore.map(str::to_owned)));
let ignored = self.state.ignored.get();
self.state.ignored.set(ignored + 1);
return;
}
}
// Looks like we've got a test that needs to be executed! Push it onto
// the list of remaining tests.
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();
// First up, try to make progress on all active tests. Remove any
// finished tests.
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());
}
// Next up, try to schedule as many tests as we can. Once we get a test
// we `poll` it once to ensure we'll receive notifications. We only
// want to schedule up to a maximum amount of work though, so this may
// not schedule all tests.
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());
}
// Tests are still executing, we're registered to get a notification,
// keep going.
if running.len() != 0 {
return Poll::Pending;
}
// If there are no tests running then we must have finished everything,
// so we shouldn't have any more remaining tests either.
assert_eq!(remaining.len(), 0);
self.0.print_results();
let all_passed = self.0.failures.borrow().len() == 0;
Poll::Ready(all_passed)
}
}
impl State {
fn log_test_result(&self, test: Test, result: TestResult) {
// Save off the test for later processing when we print the final
// results.
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.set(self.succeeded.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.set(self.succeeded.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.len() > 0 {
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));
}
}
self.formatter.writeln("");
self.formatter.writeln(&format!(
"test result: {}. \
{} passed; \
{} failed; \
{} ignored; \
{} filtered out\n",
if failures.len() == 0 { "ok" } else { "FAILED" },
self.succeeded.get(),
failures.len(),
self.ignored.get(),
self.filtered.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, 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);
}
}
/// A wrapper future around each test
///
/// This future is what's actually executed for each test and is what's stored
/// inside of a `Test`. This wrapper future performs two critical functions:
///
/// * First, every time when polled, it configures the `CURRENT_OUTPUT` tls
/// variable to capture output for the current test. That way at least when
/// we've got Rust code running we'll be able to capture output.
///
/// * Next, this "catches panics". Right now all Wasm code is configured as
/// panic=abort, but it's more like an exception in JS. It's pretty sketchy
/// to actually continue executing Rust code after an "abort", but we don't
/// have much of a choice for now.
///
/// Panics are caught here by using a shim function that is annotated with
/// `catch` so we can capture JS exceptions (which Rust panics become). This
/// way if any Rust code along the execution of a test panics we'll hopefully
/// capture it.
///
/// Note that both of the above aspects of this future are really just best
/// effort. This is all a bit of a hack right now when it comes down to it and
/// it definitely won't work in some situations. Hopefully as those situations
/// arise though we can handle them!
///
/// The good news is that everything should work flawlessly in the case where
/// tests have no output and execute successfully. And everyone always writes
/// perfect code on the first try, right? *sobs*
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();
// Use `new_unchecked` here to project our own pin, and we never
// move `test` so this should be safe
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
}