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