Skip to main content

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