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