diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97af268d19f..0417cf3994d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1202,12 +1202,19 @@ jobs: env: RUSTFLAGS: "" - - name: Test tokio for emscripten + - name: Test tokio for emscripten (JSPI) run: cargo test -p tokio --target wasm32-unknown-emscripten --features "rt,time,sync,macros,io-util,test-util" --tests working-directory: tokio env: CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUNNER: node - RUSTFLAGS: "-C link-args=-sALLOW_MEMORY_GROWTH=1 -C link-args=-sEXIT_RUNTIME=1 -C link-args=-sSTACK_SIZE=1048576" + RUSTFLAGS: "-C link-args=-sALLOW_MEMORY_GROWTH=1 -C link-args=-sEXIT_RUNTIME=1 -C link-args=-sJSPI -C link-args=-sSTACK_SIZE=1048576" + + - name: Test tokio for emscripten (non-JSPI) + run: cargo test -p tokio --target wasm32-unknown-emscripten --features "rt,time,sync,macros,io-util,test-util" --test rt_emscripten_block_on + working-directory: tokio + env: + CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUNNER: node + RUSTFLAGS: "-C link-args=-sALLOW_MEMORY_GROWTH=1 -C link-args=-sEXIT_RUNTIME=1" check-external-types: name: check-external-types (${{ matrix.os }}) diff --git a/spellcheck.dic b/spellcheck.dic index 2e01dad8402..073dfd98227 100644 --- a/spellcheck.dic +++ b/spellcheck.dic @@ -1,4 +1,4 @@ -327 +330 & + < @@ -162,6 +162,8 @@ IP IPv4 IPv6 iteratively +JS +JSPI Kotlin's latencies Lauck @@ -266,6 +268,7 @@ subfield suboptimal subprocess superset +suspendable symlink symlinks sys diff --git a/tokio-macros/src/entry.rs b/tokio-macros/src/entry.rs index 9c4f7f9c3b2..00432400359 100644 --- a/tokio-macros/src/entry.rs +++ b/tokio-macros/src/entry.rs @@ -521,10 +521,49 @@ fn parse_knobs(mut input: ItemFn, is_test: bool, config: FinalConfig) -> TokenSt }; - // Emscripten runs the native expansion; only the `multi_thread` flavor - // diverges — it has no native threads there, so it's rejected with a - // targeted error rather than the opaque failure of the native - // multi-thread `block_on`. + let output_type = match &input.sig.output { + // For functions with no return value syn doesn't print anything, + // but that doesn't work as `Output` for our boxed `Future`, so + // default to `()` (the same type as the function output). + syn::ReturnType::Default => quote! { () }, + syn::ReturnType::Type(_, ret_type) => quote! { #ret_type }, + }; + let raw_body = input.body(); + // On Emscripten `#[tokio::test]` claims exclusive JSPI suspension for the + // promising activation libtest runs it under (Emscripten auto-promising- + // wraps `main` under `-sJSPI`), so the body's `block_on` can suspend on + // the host event loop; without `-sJSPI` no guard is created (waits + // panic). + let emscripten_test_block = quote_spanned! {last_stmt_end_span=> + #[allow(clippy::expect_used, clippy::diverging_sub_expression, clippy::needless_return, clippy::unwrap_in_result)] + { + let _suspend_guard = if #crate_path::runtime::jspi_enabled() { + ::core::option::Option::Some(#crate_path::runtime::SuspendGuard::new()) + } else { + ::core::option::Option::None + }; + + let body = async #raw_body; + #crate_path::pin!(body); + let body: ::core::pin::Pin<&mut dyn ::core::future::Future> = body; + + // Inner block: keep the `use` from shadowing names in the + // user body, which shares this scope. + let rt = { + #use_builder + + #rt + .enable_all() + .#build + .expect("Failed building the Runtime") + }; + return rt.block_on(body); + } + }; + + // The `multi_thread` flavor has no native threads on Emscripten, so it's + // rejected with a targeted error (pending a `PROXY_TO_PTHREAD` runtime) + // rather than the opaque failure of the native multi-thread `block_on`. let last_block = match config.flavor { RuntimeFlavor::Threaded => quote! { #[cfg(not(target_os = "emscripten"))] @@ -536,6 +575,12 @@ fn parse_knobs(mut input: ItemFn, is_test: bool, config: FinalConfig) -> TokenSt `flavor = \"current_thread\"`" ); }, + _ if is_test => quote! { + #[cfg(not(target_os = "emscripten"))] + #native_last_block + #[cfg(target_os = "emscripten")] + #emscripten_test_block + }, _ => native_last_block, }; @@ -550,18 +595,15 @@ fn parse_knobs(mut input: ItemFn, is_test: bool, config: FinalConfig) -> TokenSt // // We don't do this for the main function as it should only be used once so // there will be no benefit. - let output_type = match &input.sig.output { - // For functions with no return value syn doesn't print anything, - // but that doesn't work as `Output` for our boxed `Future`, so - // default to `()` (the same type as the function output). - syn::ReturnType::Default => quote! { () }, - syn::ReturnType::Type(_, ret_type) => quote! { #ret_type }, - }; - let body = if is_test { + // On Emscripten the body construction lives inside the capture-free + // closure (see `emscripten_test_block`). quote! { + #[cfg(not(target_os = "emscripten"))] let body = async #body; + #[cfg(not(target_os = "emscripten"))] #crate_path::pin!(body); + #[cfg(not(target_os = "emscripten"))] let body: ::core::pin::Pin<&mut dyn ::core::future::Future> = body; } } else { diff --git a/tokio/src/lib.rs b/tokio/src/lib.rs index d98898cb6c9..471ac30c6a7 100644 --- a/tokio/src/lib.rs +++ b/tokio/src/lib.rs @@ -446,46 +446,52 @@ //! //! ### Emscripten support //! -//! The `wasm32-unknown-emscripten` target is supported at parity with the -//! other Wasm targets. A host-event-loop execution model with a parking -//! `block_on` is a planned follow-up; until it lands, a `block_on` whose -//! future cannot resolve synchronously behaves as on other single-threaded -//! Wasm targets. -//! -//! Supported features: `rt`, `time`, `sync`, `macros`, `fs`, `io-util`, -//! `io-std`, and `test-util` — the same surface as the other single-threaded -//! Wasm targets. The `net` reactor (epoll-backed, over Emscripten's socket -//! support) is planned as a follow-up; until then the `net` feature fails to -//! build for this target (rejected by `mio`). +//! The `wasm32-unknown-emscripten` target runs Tokio's canonical +//! single-threaded (`current_thread`) scheduler, time driver, and task system +//! unchanged. `Runtime::block_on` drives the scheduler to a synchronous fixed +//! point; a future that would have to suspend (await a timer or an external +//! wake) panics with a targeted error, since the host event loop cannot run +//! while Wasm blocks the stack. +//! +//! [JSPI] (-sJSPI in Emscripten) implements an exception to this behavior: +//! a drive that would block instead suspends the whole calling stack on a +//! host timer — the host loop keeps running and wakes it. Emscripten +//! auto-promising-wraps `main` under `-sJSPI`, so `#[tokio::main]` and +//! `#[tokio::test]` suspend as-is; calling `block_on` from a host callback +//! outside a promising activation traps at the engine. The drive is +//! non-reentrant, like `block_on` everywhere else. +//! +//! [JSPI]: https://github.com/WebAssembly/js-promise-integration +//! +//! Supported features: `rt`, `time`, `sync`, `macros`, `io-util`, and +//! `test-util`. The `net` reactor (epoll-backed, over Emscripten's socket +//! support) and the inline `fs`/`io-std` shims are planned as follow-ups. //! //! The `process`, `signal`, and `rt-multi-thread` features are rejected at //! compile time: `process`/`signal` have no underlying primitives (`fork`/`exec`, //! kernel signal delivery) and `rt-multi-thread` has no native threads. //! -//! `spawn_blocking` dispatches to the blocking thread pool and so behaves as on -//! the other single-threaded Wasm targets. Running `spawn_blocking` closures -//! and `fs`/stdio `std::*` calls inline over Emscripten's synchronous syscalls -//! is part of the planned host-event-loop follow-up. +//! `spawn_blocking` is unchanged: with no threadpool to dispatch to it fails +//! at runtime, as on the other single-threaded Wasm targets. //! //! Panics behave as on native: `wasm32-unknown-emscripten` defaults to //! `panic = "unwind"`, so panic recovery works, a panicking task yields //! `Err(JoinError)`, and `JoinError::is_panic` / `JoinError::into_panic` //! report the payload. //! -//! `#[tokio::test]` / `#[tokio::main]` use the native macro expansion; the -//! `multi_thread` flavor is rejected (no native threads) — use -//! `flavor = "current_thread"`. -//! +//! `#[tokio::main]` still defaults to the unsupported `multi_thread` flavor, +//! `#[tokio::main(flavor = "current_thread")]` is currently required. //! //! #### Linking and running on Emscripten //! -//! No js-library or other custom file is required, and plain `node` runs the -//! test binaries directly: +//! No js-library or custom file is required (Tokio embeds its JS glue in +//! the objects); plain `node` runs the test binaries directly: //! //! ```text //! CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUNNER="node" //! RUSTFLAGS="-C link-args=-sALLOW_MEMORY_GROWTH=1 \ //! -C link-args=-sEXIT_RUNTIME=1 \ +//! -C link-args=-sJSPI \ //! -C link-args=-sSTACK_SIZE=1048576" //! ``` //! @@ -524,12 +530,21 @@ compile_error! { ))] compile_error!("Only features sync,macros,io-util,rt,time are supported on wasm."); -// On Emscripten, `process`, `signal`, and `rt-multi-thread` compile but are -// inert, so `full` (and any dependency that enables these features) still -// builds. `process` and `signal` have no `fork`/`exec` or kernel signal +// On Emscripten, `process` and `signal` have no `fork`/`exec` or kernel signal // delivery, so their modules are compiled out (see `cfg_process!` / -// `cfg_signal!`). The multi-threaded runtime compiles but only runs under a -// `PROXY_TO_PTHREAD` build; `#[tokio::main]` steers to `current_thread`. +// `cfg_signal!`) exactly as on wasi; naming those APIs is a compile error. +// `rt-multi-thread` compiles but only runs under a `PROXY_TO_PTHREAD` build; +// `#[tokio::main]` steers to `current_thread`. + +// We currently only support the single-threaded Emscripten runtime, with +// support for JSPI. +// A `-pthread` (atomics) build breaks its assumptions, until the kernel is made +// thread-aware. +#[cfg(all(target_os = "emscripten", feature = "rt", target_feature = "atomics"))] +compile_error!( + "Tokio's `wasm32-unknown-emscripten` runtime is single-threaded and does \ +not support `-pthread` (atomics) builds." +); #[cfg(all(not(tokio_unstable), feature = "io-uring"))] compile_error!("The `io-uring` feature requires `--cfg tokio_unstable`."); diff --git a/tokio/src/macros/cfg.rs b/tokio/src/macros/cfg.rs index 86c1f199386..f8b1f8b2201 100644 --- a/tokio/src/macros/cfg.rs +++ b/tokio/src/macros/cfg.rs @@ -398,11 +398,9 @@ macro_rules! cfg_process { #[cfg_attr(docsrs, doc(cfg(feature = "process")))] #[cfg(not(loom))] #[cfg(not(target_os = "wasi"))] - // Emscripten has no `fork`/`exec`, so the `process` module is a - // throwing stub there (see `process/emscripten.rs`); it still - // compiles so dependents that name the types build. The orphan - // reaper / signal driver it would otherwise need stays off via - // `cfg_process_driver!`. + // Emscripten has no `fork`/`exec`, so the `process` module is not + // available there. + #[cfg(not(target_os = "emscripten"))] $item )* } @@ -412,9 +410,6 @@ macro_rules! cfg_process_driver { ($($item:item)*) => { #[cfg(unix)] #[cfg(not(loom))] - // The driver (orphan reaper backed by the signal handler) doesn't exist - // on Emscripten; the process module there is a throwing stub. - #[cfg(not(target_os = "emscripten"))] cfg_process! { $($item)* } } } diff --git a/tokio/src/process/emscripten.rs b/tokio/src/process/emscripten.rs deleted file mode 100644 index 093b3eb8aa1..00000000000 --- a/tokio/src/process/emscripten.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Throwing process stub for `wasm32-unknown-emscripten`. -//! -//! emscripten has no `fork`/`exec`, so there is nothing to spawn or reap: the -//! orphan reaper / signal driver the unix backend relies on is compiled out -//! (see `cfg_process_driver!`). We still provide the `imp` surface that -//! `process::mod` is generic over so dependents that merely name these types -//! keep compiling. Spawning fails before it reaches here (`std`'s own -//! `Command::spawn` returns `Unsupported` on emscripten), and the `Child` / -//! `ChildStdio` types are uninhabited — every method is unreachable. - -use crate::io::{AsyncRead, AsyncWrite, ReadBuf}; -use crate::process::kill::Kill; -use crate::process::SpawnedChild; - -use std::fmt; -use std::future::Future; -use std::io; -use std::os::fd::{AsFd, AsRawFd, BorrowedFd, IntoRawFd, OwnedFd, RawFd}; -use std::pin::Pin; -use std::process::{Child as StdChild, ExitStatus, Stdio}; -use std::task::{Context, Poll}; - -fn unsupported() -> io::Result { - Err(io::Error::new( - io::ErrorKind::Unsupported, - "spawning processes is not supported on wasm32-unknown-emscripten", - )) -} - -pub(crate) enum Child {} - -impl Child { - pub(crate) fn id(&self) -> u32 { - match *self {} - } - - pub(crate) fn try_wait(&mut self) -> io::Result> { - match *self {} - } -} - -impl fmt::Debug for Child { - fn fmt(&self, _fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self {} - } -} - -impl Kill for Child { - fn kill(&mut self) -> io::Result<()> { - match *self {} - } -} - -impl Future for Child { - type Output = io::Result; - - fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { - match *self.get_mut() {} - } -} - -pub(crate) fn build_child(_child: StdChild) -> io::Result { - unsupported() -} - -pub(crate) enum ChildStdio {} - -impl ChildStdio { - pub(crate) fn into_owned_fd(self) -> io::Result { - match self {} - } -} - -impl fmt::Debug for ChildStdio { - fn fmt(&self, _fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self {} - } -} - -impl AsRawFd for ChildStdio { - fn as_raw_fd(&self) -> RawFd { - match *self {} - } -} - -impl AsFd for ChildStdio { - fn as_fd(&self) -> BorrowedFd<'_> { - match *self {} - } -} - -impl AsyncWrite for ChildStdio { - fn poll_write( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &[u8], - ) -> Poll> { - match *self.get_mut() {} - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - match *self.get_mut() {} - } - - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - match *self.get_mut() {} - } -} - -impl AsyncRead for ChildStdio { - fn poll_read( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &mut ReadBuf<'_>, - ) -> Poll> { - match *self.get_mut() {} - } -} - -pub(crate) fn convert_to_stdio(io: ChildStdio) -> io::Result { - match io {} -} - -pub(crate) fn stdio(io: T) -> io::Result { - let _ = io.into_raw_fd(); - unsupported() -} diff --git a/tokio/src/process/mod.rs b/tokio/src/process/mod.rs index 6ca2e2b8f82..fb233cb4eb2 100644 --- a/tokio/src/process/mod.rs +++ b/tokio/src/process/mod.rs @@ -227,12 +227,7 @@ //! [`Child`]: crate::process::Child #[path = "unix/mod.rs"] -#[cfg(all(unix, not(target_os = "emscripten")))] -mod imp; - -// Emscripten has no `fork`/`exec`: a throwing stub keeps the API present. -#[path = "emscripten.rs"] -#[cfg(target_os = "emscripten")] +#[cfg(unix)] mod imp; #[cfg(unix)] diff --git a/tokio/src/runtime/jspi.rs b/tokio/src/runtime/jspi.rs new file mode 100644 index 00000000000..5f1ca488ca4 --- /dev/null +++ b/tokio/src/runtime/jspi.rs @@ -0,0 +1,96 @@ +//! Minimal JSPI primitives for `wasm32-unknown-emscripten`. +//! +//! Tokio's suspension model is strictly non-reentrant: `#[tokio::test]` claims +//! suspension for its whole promising activation with a [`SuspendGuard`], +//! and [`sleep`] — the one suspending import the runtime issues — parks +//! that activation on a host timer. With never more than one suspension in +//! flight, no spill-stack save/restore is needed: any Wasm re-entered +//! while the activation is parked completes before the resume, leaving the +//! spill stack above the suspended frame untouched. + +use std::cell::Cell; +use std::time::Duration; + +thread_local! { + static SUSPENDABLE: Cell = const { Cell::new(false) }; +} + +/// Marks the `#[tokio::test]` promising activation as suspendable for the +/// body's extent; `Drop` clears the flag across panic unwinds. Internal to +/// the test expansion, not a user convention. +#[derive(Debug)] +pub struct SuspendGuard(()); + +impl SuspendGuard { + /// Marks the current activation suspendable until drop. + #[allow(clippy::new_without_default)] + pub fn new() -> SuspendGuard { + SUSPENDABLE.set(true); + SuspendGuard(()) + } +} + +impl Drop for SuspendGuard { + fn drop(&mut self) { + SUSPENDABLE.set(false); + } +} + +/// Whether the park leaf may suspend: a [`SuspendGuard`] is live. +pub(crate) fn can_suspend() -> bool { + SUSPENDABLE.get() +} + +// Emscripten EM_JS convention: `__em_js__` data exports carry JS +// bodies into the objects, and `__asyncjs__` names get +// `WebAssembly.Suspending` treatment under `-sJSPI`. The static must be +// referenced from linked code (`anchor`) so its archive member is pulled +// in. +const TOKIO_JSPI_SLEEP: &str = "(ms)<::>{ return Asyncify.handleAsync(async () => { await new Promise((r) => setTimeout(r, ms)); }); }"; + +const fn em_js(s: &str) -> [u8; N] { + // NUL-terminated: N == s.len() + 1 + let mut a = [0u8; N]; + let b = s.as_bytes(); + let mut i = 0; + while i < b.len() { + a[i] = b[i]; + i += 1; + } + a +} + +#[allow(non_upper_case_globals)] +#[no_mangle] +#[used] +static __em_js____asyncjs__tokio_jspi_sleep: [u8; TOKIO_JSPI_SLEEP.len() + 1] = + em_js(TOKIO_JSPI_SLEEP); + +unsafe extern "C" { + /// Reports the `ASYNCIFY` build mode: 0 = none, 1 = `asyncify`, 2 = JSPI. + safe fn emscripten_has_asyncify() -> i32; +} + +// Suspending import: parks on a host timeout. Unit return, never rejects, +// `Asyncify.handleAsync` keeps the runtime alive across the suspension. +#[link(wasm_import_module = "env")] +unsafe extern "C-unwind" { + #[link_name = "__asyncjs__tokio_jspi_sleep"] + safe fn tokio_jspi_sleep_import(ms: f64); +} + +#[inline(never)] +fn anchor() { + std::hint::black_box(__em_js____asyncjs__tokio_jspi_sleep.as_ptr()); +} + +/// Whether JSPI suspension is available: linked with `-sJSPI`. +pub fn jspi_enabled() -> bool { + emscripten_has_asyncify() == 2 +} + +/// Suspend the owning activation for `dur` on a host timer. +pub(crate) fn sleep(dur: Duration) { + anchor(); + tokio_jspi_sleep_import(dur.as_secs_f64() * 1000.0); +} diff --git a/tokio/src/runtime/mod.rs b/tokio/src/runtime/mod.rs index 05bb5fa6ee1..84af32599f6 100644 --- a/tokio/src/runtime/mod.rs +++ b/tokio/src/runtime/mod.rs @@ -416,6 +416,16 @@ mod tests; pub(crate) mod context; +#[cfg(all(target_os = "emscripten", feature = "rt"))] +pub(crate) mod jspi; + +// Used by the `#[tokio::test]` expansion on Emscripten: the guard claims +// JSPI suspension for the test's promising activation so the body's +// `block_on` can suspend on the host loop. Not public API. +#[cfg(all(target_os = "emscripten", feature = "rt", feature = "test-util"))] +#[doc(hidden)] +pub use jspi::{jspi_enabled, SuspendGuard}; + pub(crate) mod park; pub(crate) mod driver; @@ -550,6 +560,8 @@ cfg_rt! { } cfg_fs! { + // Emscripten's `fs` uses the inline blocking shim, not the pool. + #[cfg_attr(target_os = "emscripten", allow(unused_imports))] pub(crate) use blocking::spawn_mandatory_blocking; } diff --git a/tokio/src/runtime/park.rs b/tokio/src/runtime/park.rs index cf502c40408..7a638e813eb 100644 --- a/tokio/src/runtime/park.rs +++ b/tokio/src/runtime/park.rs @@ -28,6 +28,7 @@ const EMPTY: usize = 0; const PARKED: usize = 1; const NOTIFIED: usize = 2; +#[cfg(not(all(target_os = "emscripten", feature = "rt")))] tokio_thread_local! { static CURRENT_PARKER: ParkThread = ParkThread::new(); } @@ -76,6 +77,7 @@ impl ParkThread { // ==== impl Inner ==== impl Inner { + #[cfg(not(all(target_os = "emscripten", feature = "rt")))] fn park(&self) { // If we were previously notified then we consume this notification and // return quickly. @@ -123,7 +125,41 @@ impl Inner { } } - /// Parks the current thread for at most `dur`. + #[cfg(all(target_os = "emscripten", feature = "rt"))] + fn park(&self) { + // If we were previously notified then we consume this notification and + // return quickly. + if self + .state + .compare_exchange(NOTIFIED, EMPTY, SeqCst, SeqCst) + .is_ok() + { + return; + } + + // On Emscripten a would-block wait with no deadline can never be + // woken: host timers are the only mid-park wake source (tokio has + // no reactor here, and every tokio-internal waker fires during the + // drive, before the park). Fail fast instead of deadlocking, + // inside or outside a JSPI root alike. + panic!( + "cannot block to wait for an external wake on \ + wasm32-unknown-emscripten: no source exists that could deliver \ + the wake (host timers are the only mid-park wake source, and \ + this wait has no deadline)" + ); + } + + /// Consume a pending notification token, if any. + #[cfg(all(target_os = "emscripten", feature = "rt"))] + fn consume_notified(&self) -> bool { + self.state + .compare_exchange(NOTIFIED, EMPTY, SeqCst, SeqCst) + .is_ok() + } + + /// Parks the current thread for at most `dur` (native condvar path). + #[cfg(not(all(target_os = "emscripten", feature = "rt")))] fn park_timeout(&self, dur: Duration) { // Like `park` above we have a fast path for an already-notified thread, // and afterwards we start coordinating for a sleep. Return quickly. @@ -174,6 +210,42 @@ impl Inner { } } + /// Parks the current thread for at most `dur`. + #[cfg(all(target_os = "emscripten", feature = "rt"))] + fn park_timeout(&self, dur: Duration) { + // Like `park` above we have a fast path for an already-notified thread, + // and afterwards we start coordinating for a sleep. Return quickly. + if self + .state + .compare_exchange(NOTIFIED, EMPTY, SeqCst, SeqCst) + .is_ok() + { + return; + } + + // Emscripten: with a live suspend guard, waiting is real — + // suspend on a host timer (consume a notification delivered during + // the sleep so the token doesn't leak into the next park). Otherwise + // a zero-duration park is a no-op, and any real wait is impossible + // (suspending would trap, busy-waiting would starve the host loop + // every wake depends on) so it panics. + if crate::runtime::jspi::can_suspend() { + // Includes `dur == 0`: a genuine host turn, letting timers + // and microtasks run mid-drive (the scheduler's maintenance + // yield). + crate::runtime::jspi::sleep(dur); + let _ = self.consume_notified(); + } else if dur == Duration::from_millis(0) { + // Native semantics: a zero-duration park returns immediately. + } else { + panic!( + "cannot block for a timed wait on wasm32-unknown-emscripten \ + outside a JSPI activation: link with `-sJSPI` and run inside \ + `#[tokio::test]` to suspend on the host event loop" + ); + } + } + fn unpark(&self) { // To ensure the unparked thread will observe any writes we made before // this call, we must perform a release operation that `park` can @@ -231,6 +303,12 @@ use std::task::{RawWaker, RawWakerVTable, Waker}; /// Blocks the current thread using a condition variable. #[derive(Debug)] pub(crate) struct CachedParkThread { + // While the sole JSPI activation is parked, host callbacks can still + // run tokio code on this thread; a shared thread-local parker could + // hand this stack's notification token to that code. Each blocking + // call gets its own parker instead. + #[cfg(all(target_os = "emscripten", feature = "rt"))] + park: ParkThread, _anchor: PhantomData>, } @@ -241,6 +319,8 @@ impl CachedParkThread { /// the thread that the caller intends to park. pub(crate) fn new() -> CachedParkThread { CachedParkThread { + #[cfg(all(target_os = "emscripten", feature = "rt"))] + park: ParkThread::new(), _anchor: PhantomData, } } @@ -268,7 +348,12 @@ impl CachedParkThread { where F: FnOnce(&ParkThread) -> R, { - CURRENT_PARKER.try_with(|inner| f(inner)) + #[cfg(not(all(target_os = "emscripten", feature = "rt")))] + return CURRENT_PARKER.try_with(|inner| f(inner)); + + // See the field comment for Emscripten on `park` + #[cfg(all(target_os = "emscripten", feature = "rt"))] + return Ok(f(&self.park)); } pub(crate) fn block_on(&mut self, f: F) -> Result { diff --git a/tokio/tests/join_handle_panic.rs b/tokio/tests/join_handle_panic.rs index 248d5702f68..0fa50d81d61 100644 --- a/tokio/tests/join_handle_panic.rs +++ b/tokio/tests/join_handle_panic.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] // Wasi doesn't support panic recovery +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(panic = "unwind")] struct PanicsOnDrop; diff --git a/tokio/tests/rt_basic.rs b/tokio/tests/rt_basic.rs index 3fab2649a12..ca951676faf 100644 --- a/tokio/tests/rt_basic.rs +++ b/tokio/tests/rt_basic.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use tokio::runtime::{self, Runtime}; use tokio::sync::oneshot; @@ -249,7 +259,10 @@ fn spawn_two() { } } -#[cfg_attr(target_os = "wasi", ignore = "WASI: std::thread::spawn not supported")] +#[cfg_attr( + target_family = "wasm", + ignore = "wasm targets do not support std::thread::spawn" +)] #[test] fn spawn_remote() { let rt = rt(); @@ -346,7 +359,10 @@ mod unstable { } #[test] - #[cfg_attr(target_os = "wasi", ignore = "Wasi does not support panic recovery")] + #[cfg_attr( + target_family = "wasm", + ignore = "wasm targets do not support std::thread::spawn" + )] fn spawns_do_nothing() { use std::sync::Arc; @@ -375,7 +391,10 @@ mod unstable { } #[test] - #[cfg_attr(target_os = "wasi", ignore = "Wasi does not support panic recovery")] + #[cfg_attr( + target_family = "wasm", + ignore = "wasm targets do not support std::thread::spawn" + )] fn shutdown_all_concurrent_block_on() { const N: usize = 2; use std::sync::{mpsc, Arc}; @@ -479,6 +498,10 @@ fn rt() -> Runtime { } #[test] +#[cfg_attr( + target_os = "emscripten", + ignore = "on_thread_park never fires on emscripten (kernel suspends to the JS event loop instead of parking)" +)] fn before_park_yields() { use futures::task::ArcWake; use std::sync::Arc; diff --git a/tokio/tests/rt_common_before_park.rs b/tokio/tests/rt_common_before_park.rs index 74a28163225..5d37e31fd65 100644 --- a/tokio/tests/rt_common_before_park.rs +++ b/tokio/tests/rt_common_before_park.rs @@ -1,6 +1,9 @@ #![warn(rust_2018_idioms)] #![cfg(feature = "full")] #![cfg(not(target_os = "wasi"))] // Wasi doesn't support threads + // Not widened to emscripten: every test here exercises the `on_thread_park` + // Builder hook, which never fires on emscripten (the kernel suspends to the + // JS event loop instead of parking), plus a cross-thread wake. use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; diff --git a/tokio/tests/rt_emscripten_block_on.rs b/tokio/tests/rt_emscripten_block_on.rs new file mode 100644 index 00000000000..02c6518f872 --- /dev/null +++ b/tokio/tests/rt_emscripten_block_on.rs @@ -0,0 +1,73 @@ +//! `Runtime::block_on` outside a JSPI promising activation drives the +//! scheduler synchronously to a fixed point: immediate futures return their +//! value; any wait panics at the park leaf, in both CI lanes (JSPI linked +//! and not), since only the `#[tokio::test]` activation can suspend (see +//! `rt_emscripten_jspi`). + +#![cfg(all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "macros" +))] + +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::time::Duration; + +use tokio::runtime::Builder; + +fn rt() -> tokio::runtime::Runtime { + Builder::new_current_thread().enable_all().build().unwrap() +} + +/// Assert `f` panics with the targeted would-suspend message. +fn assert_panics_cannot_block_on(f: impl FnOnce()) { + let err = catch_unwind(AssertUnwindSafe(f)).expect_err("expected a would-suspend panic"); + let msg = err + .downcast_ref::() + .map(String::as_str) + .or_else(|| err.downcast_ref::<&str>().copied()) + .unwrap_or(""); + assert!( + msg.contains("cannot block"), + "unexpected panic message: {msg}" + ); +} + +#[test] +fn block_on_returns_immediate_value() { + let out = rt().block_on(async { 1 + 2 }); + assert_eq!(out, 3); +} + +#[test] +fn block_on_drives_ready_spawned_tasks() { + // Spawned tasks that complete synchronously must be driven to + // completion within the same fixed-point pump. + let out = rt().block_on(async { + let a = tokio::spawn(async { 20 }); + let b = tokio::spawn(async { 22 }); + a.await.unwrap() + b.await.unwrap() + }); + assert_eq!(out, 42); +} + +#[test] +fn timer_wait_panics_outside_activation() { + assert_panics_cannot_block_on(|| { + rt().block_on(async { + tokio::time::sleep(Duration::from_millis(10)).await; + }); + }); +} + +#[test] +fn external_wait_panics_outside_activation() { + // A oneshot whose sender never fires: pending with no wake source. + assert_panics_cannot_block_on(|| { + rt().block_on(async { + let (_tx, rx) = tokio::sync::oneshot::channel::<()>(); + let _ = rx.await; + }); + }); +} diff --git a/tokio/tests/rt_emscripten_jspi.rs b/tokio/tests/rt_emscripten_jspi.rs new file mode 100644 index 00000000000..d51ea7060d7 --- /dev/null +++ b/tokio/tests/rt_emscripten_jspi.rs @@ -0,0 +1,121 @@ +//! JSPI suspension contracts. With `-sJSPI` a would-block wait in a +//! `#[tokio::test]` body suspends on a host timer while the host loop +//! delivers wakes. Without it a wait throws (see `rt_emscripten_block_on`). +//! +//! NOTE: This is the only Emscripten test file with real timer tests. + +#![cfg(all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros" +))] + +use std::sync::Arc; +use std::time::Duration; + +use tokio::runtime::Builder; +use tokio::sync::Notify; +use tokio::time::{sleep, Instant}; + +fn rt() -> tokio::runtime::Runtime { + Builder::new_current_thread().enable_all().build().unwrap() +} + +#[test] +fn block_on_yield_now_takes_a_host_turn() { + let out = rt().block_on(async { + tokio::task::yield_now().await; + 7 + }); + assert_eq!(out, 7); +} + +#[tokio::test] +async fn root_sleep_parks_and_resumes() { + let start = tokio::time::Instant::now(); + tokio::time::sleep(Duration::from_millis(20)).await; + assert!( + start.elapsed() >= Duration::from_millis(15), + "the park must actually wait out the timer deadline" + ); +} + +#[tokio::test] +async fn root_spawned_tasks_with_timers() { + let out = async { + let a = tokio::spawn(async { + tokio::time::sleep(Duration::from_millis(5)).await; + 20 + }); + let b = tokio::spawn(async { + tokio::time::sleep(Duration::from_millis(10)).await; + 22 + }); + a.await.unwrap() + b.await.unwrap() + } + .await; + assert_eq!(out, 42); +} + +#[tokio::test] +async fn sequential_parks_inside_one_root() { + // Each park must suspend and resume independently; leaf bookkeeping + // must balance across them. + for i in 0..3u32 { + let start = tokio::time::Instant::now(); + tokio::time::sleep(Duration::from_millis(2)).await; + assert!(start.elapsed() >= Duration::from_millis(1), "park {i}"); + } +} + +#[tokio::test] +async fn root_park_resumes_on_timer_driven_wake() { + // The spawned task's timer bounds the driver park; on resume it sends + // and wakes the root future. + let (tx, rx) = tokio::sync::oneshot::channel::(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(5)).await; + tx.send(11).unwrap(); + }); + assert_eq!(rx.await.unwrap(), 11); +} + +// A self-rewaking task must not starve the real host timer: the +// event-interval park yields a 0ms host turn so the timer still fires. +#[tokio::test] +async fn greedy_task_does_not_starve_host_timer() { + tokio::spawn(async { + loop { + tokio::task::yield_now().await; + } + }); + sleep(Duration::from_millis(5)).await; +} + +// When a nearer timer fires, the next park must re-arm for a still-pending +// farther timer rather than dropping it. +#[tokio::test] +async fn farther_timer_survives_nearer_timer_firing() { + let start = Instant::now(); + + let notify = Arc::new(Notify::new()); + let n = notify.clone(); + let near = tokio::spawn(async move { + sleep(Duration::from_millis(5)).await; + n.notify_one(); + }); + let waiter = tokio::spawn(async move { + notify.notified().await; + }); + + sleep(Duration::from_millis(25)).await; + assert!( + start.elapsed() >= Duration::from_millis(25), + "farther timer did not hold its deadline" + ); + + near.await.unwrap(); + waiter.await.unwrap(); +} diff --git a/tokio/tests/rt_handle.rs b/tokio/tests/rt_handle.rs index 8feb7207d0b..b391e929da5 100644 --- a/tokio/tests/rt_handle.rs +++ b/tokio/tests/rt_handle.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::sync::Arc; use tokio::runtime::Runtime; diff --git a/tokio/tests/rt_panic.rs b/tokio/tests/rt_panic.rs index 1bf05580ab6..a1650e92941 100644 --- a/tokio/tests/rt_panic.rs +++ b/tokio/tests/rt_panic.rs @@ -1,11 +1,23 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(not(target_os = "wasi"))] // Wasi doesn't support panic recovery #![cfg(panic = "unwind")] use futures::future; use std::error::Error; -use tokio::runtime::{Builder, Handle, Runtime}; +#[cfg(not(target_os = "emscripten"))] +use tokio::runtime::Builder; +use tokio::runtime::{Handle, Runtime}; mod support { pub mod panic; @@ -47,6 +59,7 @@ fn into_panic_panic_caller() -> Result<(), Box> { } #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn builder_worker_threads_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let _ = Builder::new_multi_thread().worker_threads(0).build(); @@ -59,6 +72,7 @@ fn builder_worker_threads_panic_caller() -> Result<(), Box> { } #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn builder_max_blocking_threads_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let _ = Builder::new_multi_thread().max_blocking_threads(0).build(); @@ -71,6 +85,7 @@ fn builder_max_blocking_threads_panic_caller() -> Result<(), Box> { } #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn builder_global_queue_interval_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let _ = Builder::new_multi_thread().global_queue_interval(0).build(); @@ -83,6 +98,7 @@ fn builder_global_queue_interval_panic_caller() -> Result<(), Box> { } #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn builder_event_interval_interval_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let _ = Builder::new_multi_thread().event_interval(0).build(); @@ -95,6 +111,7 @@ fn builder_event_interval_interval_panic_caller() -> Result<(), Box> } #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn builder_name_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let _ = Builder::new_multi_thread().name(" ").build(); diff --git a/tokio/tests/rt_poll_callbacks.rs b/tokio/tests/rt_poll_callbacks.rs index 8ccff385772..f0996ff1b73 100644 --- a/tokio/tests/rt_poll_callbacks.rs +++ b/tokio/tests/rt_poll_callbacks.rs @@ -5,7 +5,7 @@ use std::sync::{atomic::AtomicUsize, Arc, Mutex}; use tokio::task::yield_now; -#[cfg(not(target_os = "wasi"))] +#[cfg(not(target_family = "wasm"))] #[test] fn callbacks_fire_multi_thread() { let poll_start_counter = Arc::new(AtomicUsize::new(0)); diff --git a/tokio/tests/rt_shutdown_err.rs b/tokio/tests/rt_shutdown_err.rs index b92d8eb24f1..b0203b18787 100644 --- a/tokio/tests/rt_shutdown_err.rs +++ b/tokio/tests/rt_shutdown_err.rs @@ -1,8 +1,19 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(not(miri))] // No socket in miri. use std::io; +#[cfg(not(target_family = "wasm"))] use tokio::net::TcpListener; use tokio::runtime::Builder; @@ -10,6 +21,7 @@ fn rt() -> tokio::runtime::Runtime { Builder::new_current_thread().enable_all().build().unwrap() } +#[cfg(not(target_family = "wasm"))] // needs net (TcpListener) #[test] fn test_is_rt_shutdown_err() { let rt1 = rt(); diff --git a/tokio/tests/rt_time_start_paused.rs b/tokio/tests/rt_time_start_paused.rs index 1765d625e19..8bd66005bed 100644 --- a/tokio/tests/rt_time_start_paused.rs +++ b/tokio/tests/rt_time_start_paused.rs @@ -1,4 +1,14 @@ -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use tokio::time::{Duration, Instant}; diff --git a/tokio/tests/sync_once_cell.rs b/tokio/tests/sync_once_cell.rs index a05438f24c8..a4a9bfb584d 100644 --- a/tokio/tests/sync_once_cell.rs +++ b/tokio/tests/sync_once_cell.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::mem; use std::sync::atomic::{AtomicU32, Ordering}; diff --git a/tokio/tests/sync_panic.rs b/tokio/tests/sync_panic.rs index c781c846bf5..9bb5a5afce6 100644 --- a/tokio/tests/sync_panic.rs +++ b/tokio/tests/sync_panic.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(panic = "unwind")] use std::{error::Error, sync::Arc}; diff --git a/tokio/tests/sync_set_once.rs b/tokio/tests/sync_set_once.rs index 5b6d88a9f51..3ffdfd1882e 100644 --- a/tokio/tests/sync_set_once.rs +++ b/tokio/tests/sync_set_once.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::sync::{ atomic::{AtomicU32, Ordering}, diff --git a/tokio/tests/task_abort.rs b/tokio/tests/task_abort.rs index 8de366454e0..d0877a20a65 100644 --- a/tokio/tests/task_abort.rs +++ b/tokio/tests/task_abort.rs @@ -1,16 +1,29 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] // Wasi doesn't support panic recovery - +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] + +#[cfg(not(target_family = "wasm"))] use std::sync::Arc; +#[cfg(not(target_family = "wasm"))] use std::thread::sleep; +#[cfg(not(target_family = "wasm"))] use tokio::time::Duration; use tokio::runtime::Builder; -#[cfg(panic = "unwind")] +#[cfg(all(panic = "unwind", not(target_family = "wasm")))] struct PanicOnDrop; -#[cfg(panic = "unwind")] +#[cfg(all(panic = "unwind", not(target_family = "wasm")))] impl Drop for PanicOnDrop { fn drop(&mut self) { panic!("Well what did you expect would happen..."); @@ -19,6 +32,7 @@ impl Drop for PanicOnDrop { /// Checks that a suspended task can be aborted without panicking as reported in /// issue #3157: . +#[cfg(not(target_family = "wasm"))] #[test] fn test_abort_without_panic_3157() { let rt = Builder::new_multi_thread() @@ -41,6 +55,7 @@ fn test_abort_without_panic_3157() { /// Checks that a suspended task can be aborted inside of a current_thread /// executor without panicking as reported in issue #3662: /// . +#[cfg(not(target_family = "wasm"))] #[test] fn test_abort_without_panic_3662() { use std::sync::atomic::{AtomicBool, Ordering}; @@ -104,6 +119,7 @@ fn test_abort_without_panic_3662() { /// Checks that a suspended LocalSet task can be aborted from a remote thread /// without panicking and without running the tasks destructor on the wrong thread. /// +#[cfg(not(target_family = "wasm"))] #[test] fn remote_abort_local_set_3929() { struct DropCheck { @@ -147,6 +163,7 @@ fn remote_abort_local_set_3929() { /// Checks that a suspended task can be aborted even if the `JoinHandle` is immediately dropped. /// issue #3964: . +#[cfg(not(target_family = "wasm"))] #[test] fn test_abort_wakes_task_3964() { let rt = Builder::new_current_thread().enable_time().build().unwrap(); @@ -177,8 +194,8 @@ fn test_abort_wakes_task_3964() { /// Checks that aborting a task whose destructor panics does not allow the /// panic to escape the task. +#[cfg(all(panic = "unwind", not(target_family = "wasm")))] #[test] -#[cfg(panic = "unwind")] fn test_abort_task_that_panics_on_drop_contained() { let rt = Builder::new_current_thread().enable_time().build().unwrap(); @@ -201,8 +218,8 @@ fn test_abort_task_that_panics_on_drop_contained() { } /// Checks that aborting a task whose destructor panics has the expected result. +#[cfg(all(panic = "unwind", not(target_family = "wasm")))] #[test] -#[cfg(panic = "unwind")] fn test_abort_task_that_panics_on_drop_returned() { let rt = Builder::new_current_thread().enable_time().build().unwrap(); diff --git a/tokio/tests/task_emscripten.rs b/tokio/tests/task_emscripten.rs new file mode 100644 index 00000000000..3ff2b542355 --- /dev/null +++ b/tokio/tests/task_emscripten.rs @@ -0,0 +1,60 @@ +//! Regression tests for emscripten-specific behavior: the `u64`-microseconds +//! `Instant` (whose f64-milliseconds predecessor broke exact arithmetic) and +//! the unsupported-`spawn_blocking` contract. Broader async coverage comes +//! from the standard suite, which runs on emscripten. + +#![cfg(target_os = "emscripten")] + +use std::time::Duration; + +use tokio::time::Instant; + +#[test] +fn instant_now_works() { + let now = Instant::now(); + let _ = now.elapsed(); +} + +#[test] +fn instant_comparison() { + let a = Instant::now(); + for _ in 0..1000 { + std::hint::black_box(()); + } + let b = Instant::now(); + assert!(b >= a, "later instant should be >= earlier instant"); +} + +#[test] +fn instant_arithmetic() { + // Pins the round-trip property `(t + d) - d == t` that the original + // f64-milliseconds Instant impl broke. The u64-microseconds storage + // makes this exact. + let now = Instant::now(); + let duration = Duration::from_millis(100); + let later = now + duration; + assert!(later > now); + let diff = later - now; + assert_eq!(diff, duration); + let back = later - duration; + assert_eq!(back, now); +} + +#[test] +fn instant_with_seconds() { + let now = Instant::now(); + let duration = Duration::from_secs(1); + let later = now + duration; + let elapsed = later.duration_since(now); + assert_eq!(elapsed.as_secs(), 1); +} + +/// There is no threadpool on a single-threaded JS worker: `spawn_blocking` +/// is unsupported on emscripten, as on the other single-threaded wasm +/// targets. `tokio::fs` and `tokio::io::{stdin, stdout, stderr}` do not rely +/// on it there — their syscalls complete synchronously. +#[tokio::test] +#[should_panic = "OS can't spawn worker thread"] +async fn spawn_blocking_is_unsupported() { + let _ = tokio::task::spawn_blocking(|| 42).await; +} diff --git a/tokio/tests/task_id.rs b/tokio/tests/task_id.rs index 0cbf80d5ace..b54dda7b42b 100644 --- a/tokio/tests/task_id.rs +++ b/tokio/tests/task_id.rs @@ -1,10 +1,21 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::error::Error; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; +#[cfg(not(target_family = "wasm"))] use tokio::runtime::Runtime; use tokio::sync::oneshot; use tokio::task::{self, Id, LocalSet}; @@ -21,7 +32,7 @@ async fn task_id_spawn() { .unwrap(); } -#[cfg(not(target_os = "wasi"))] +#[cfg(not(any(target_os = "wasi", target_os = "emscripten")))] #[tokio::test(flavor = "current_thread")] async fn task_id_spawn_blocking() { task::spawn_blocking(|| println!("task id: {}", task::id())) @@ -38,7 +49,7 @@ async fn task_id_collision_current_thread() { assert_ne!(id1.unwrap(), id2.unwrap()); } -#[cfg(not(target_os = "wasi"))] +#[cfg(not(target_family = "wasm"))] #[tokio::test(flavor = "multi_thread")] async fn task_id_collision_multi_thread() { let handle1 = tokio::spawn(async { task::id() }); @@ -59,7 +70,7 @@ async fn task_ids_match_current_thread() { handle.await.unwrap(); } -#[cfg(not(target_os = "wasi"))] +#[cfg(not(target_family = "wasm"))] #[tokio::test(flavor = "multi_thread")] async fn task_ids_match_multi_thread() { let (tx, rx) = oneshot::channel(); @@ -72,7 +83,8 @@ async fn task_ids_match_multi_thread() { } #[cfg(not(target_os = "wasi"))] -#[tokio::test(flavor = "multi_thread")] +#[cfg_attr(target_os = "emscripten", tokio::test(flavor = "current_thread"))] +#[cfg_attr(not(target_os = "emscripten"), tokio::test(flavor = "multi_thread"))] async fn task_id_future_destructor_completion() { struct MyFuture { tx: Option>, @@ -100,7 +112,8 @@ async fn task_id_future_destructor_completion() { } #[cfg(not(target_os = "wasi"))] -#[tokio::test(flavor = "multi_thread")] +#[cfg_attr(target_os = "emscripten", tokio::test(flavor = "current_thread"))] +#[cfg_attr(not(target_os = "emscripten"), tokio::test(flavor = "multi_thread"))] async fn task_id_future_destructor_abort() { struct MyFuture { tx: Option>, @@ -205,7 +218,7 @@ fn task_try_id_outside_task() { assert_eq!(None, task::try_id()); } -#[cfg(not(target_os = "wasi"))] +#[cfg(not(target_family = "wasm"))] #[test] fn task_try_id_inside_block_on() { let rt = Runtime::new().unwrap(); @@ -248,7 +261,7 @@ async fn task_id_nested_spawn_local() { .await; } -#[cfg(not(target_os = "wasi"))] +#[cfg(not(target_family = "wasm"))] #[tokio::test(flavor = "multi_thread")] async fn task_id_block_in_place_block_on_spawn() { use tokio::runtime::Builder; @@ -283,6 +296,7 @@ fn task_id_outside_task_panic_caller() -> Result<(), Box> { Ok(()) } +#[cfg(not(target_family = "wasm"))] #[test] #[cfg_attr(not(panic = "unwind"), ignore)] fn task_id_inside_block_on_panic_caller() -> Result<(), Box> { diff --git a/tokio/tests/task_join_set.rs b/tokio/tests/task_join_set.rs index 38534d1b734..203c2e2510c 100644 --- a/tokio/tests/task_join_set.rs +++ b/tokio/tests/task_join_set.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use futures::future::{pending, FutureExt}; use std::panic; @@ -433,6 +443,7 @@ mod spawn_local { set.spawn_local(async {}); } + #[cfg(not(target_family = "wasm"))] #[tokio::test(flavor = "multi_thread")] #[should_panic( expected = "`spawn_local` called from outside of a `task::LocalSet` or `runtime::LocalRuntime`" diff --git a/tokio/tests/task_local.rs b/tokio/tests/task_local.rs index be9de724163..e39ad862a81 100644 --- a/tokio/tests/task_local.rs +++ b/tokio/tests/task_local.rs @@ -1,11 +1,22 @@ -#![cfg(all(feature = "full", not(target_os = "wasi")))] // Wasi doesn't support threads +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use tokio::sync::oneshot; -#[tokio::test(flavor = "multi_thread")] +#[cfg_attr(not(target_family = "wasm"), tokio::test(flavor = "multi_thread"))] +#[cfg_attr(target_family = "wasm", tokio::test)] async fn local() { tokio::task_local! { static REQ_ID: u32; diff --git a/tokio/tests/task_panic.rs b/tokio/tests/task_panic.rs index 8b4de2ada54..5747f69add1 100644 --- a/tokio/tests/task_panic.rs +++ b/tokio/tests/task_panic.rs @@ -1,11 +1,23 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(panic = "unwind")] use futures::future; use std::error::Error; use tokio::runtime::Builder; -use tokio::task::{self, block_in_place}; +use tokio::task; +#[cfg(not(target_os = "emscripten"))] +use tokio::task::block_in_place; mod support { pub mod panic; @@ -13,6 +25,7 @@ mod support { use support::panic::test_panic; #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn block_in_place_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let rt = Builder::new_current_thread().enable_all().build().unwrap(); diff --git a/tokio/tests/task_yield_now.rs b/tokio/tests/task_yield_now.rs index e6fe5d2009a..343b9418125 100644 --- a/tokio/tests/task_yield_now.rs +++ b/tokio/tests/task_yield_now.rs @@ -1,4 +1,11 @@ -#![cfg(all(feature = "full", not(target_os = "wasi"), tokio_unstable))] +#![cfg(all( + any( + feature = "full", + all(target_os = "emscripten", feature = "rt", feature = "macros") + ), + not(target_os = "wasi"), + tokio_unstable +))] use tokio::task; use tokio_test::task::spawn; @@ -15,6 +22,7 @@ fn yield_now_outside_of_runtime() { assert!(task.poll().is_ready()); } +#[cfg(not(target_family = "wasm"))] #[tokio::test(flavor = "multi_thread")] async fn yield_now_external_executor_and_block_in_place() { let j = tokio::spawn(async { diff --git a/tokio/tests/test_clock.rs b/tokio/tests/test_clock.rs index 891636fdb28..79c69d019e3 100644 --- a/tokio/tests/test_clock.rs +++ b/tokio/tests/test_clock.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use tokio::time::{self, Duration, Instant}; diff --git a/tokio/tests/time_interval.rs b/tokio/tests/time_interval.rs index 7472a37123c..8138b954ba1 100644 --- a/tokio/tests/time_interval.rs +++ b/tokio/tests/time_interval.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::pin::Pin; use std::task::{Context, Poll}; diff --git a/tokio/tests/time_panic.rs b/tokio/tests/time_panic.rs index 918d02a416e..8ac21e7247d 100644 --- a/tokio/tests/time_panic.rs +++ b/tokio/tests/time_panic.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] // Wasi doesn't support panic recovery +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(panic = "unwind")] use futures::future; @@ -22,21 +32,24 @@ fn rt_combinations() -> Vec { .unwrap(); rts.push(rt); - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(1) - .enable_all() - .build() - .unwrap(); - rts.push(rt); + #[cfg(not(target_os = "emscripten"))] + { + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .build() + .unwrap(); + rts.push(rt); - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(4) - .enable_all() - .build() - .unwrap(); - rts.push(rt); + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(4) + .enable_all() + .build() + .unwrap(); + rts.push(rt); + } - #[cfg(tokio_unstable)] + #[cfg(all(tokio_unstable, not(target_os = "emscripten")))] { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(1) diff --git a/tokio/tests/time_pause.rs b/tokio/tests/time_pause.rs index be993b4c8dd..3ca5be6ea20 100644 --- a/tokio/tests/time_pause.rs +++ b/tokio/tests/time_pause.rs @@ -1,13 +1,25 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(not(miri))] // Too slow on miri. +#[cfg(not(target_os = "emscripten"))] use rand::SeedableRng; +#[cfg(not(target_os = "emscripten"))] use rand::{rngs::StdRng, Rng}; use tokio::time::{self, Duration, Instant, Sleep}; use tokio_test::{assert_elapsed, assert_pending, assert_ready, assert_ready_eq, task}; -#[cfg(not(target_os = "wasi"))] +#[cfg(all(feature = "full", not(target_os = "wasi")))] use tokio_test::assert_err; use std::{ @@ -48,6 +60,10 @@ async fn pause_time_in_spawn_threads() { assert_err!(t.await); } +// `#[tokio::main]` returning a value isn't supported on emscripten: the +// worker entry can only signal completion, not marshal a return value back +// across the worker boundary. +#[cfg(not(target_os = "emscripten"))] #[test] fn paused_time_is_deterministic() { let run_1 = paused_time_stress_run(); @@ -56,6 +72,7 @@ fn paused_time_is_deterministic() { assert_eq!(run_1, run_2); } +#[cfg(not(target_os = "emscripten"))] #[tokio::main(flavor = "current_thread", start_paused = true)] async fn paused_time_stress_run() -> Vec { let mut rng = StdRng::seed_from_u64(1); diff --git a/tokio/tests/time_sleep.rs b/tokio/tests/time_sleep.rs index b82b1cc6ae4..3fe6c48d236 100644 --- a/tokio/tests/time_sleep.rs +++ b/tokio/tests/time_sleep.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(not(miri))] // Too slow on Miri. use std::future::Future; diff --git a/tokio/tests/time_timeout.rs b/tokio/tests/time_timeout.rs index ec871cf62fe..0959392ca1f 100644 --- a/tokio/tests/time_timeout.rs +++ b/tokio/tests/time_timeout.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use tokio::sync::oneshot; use tokio::time::{self, timeout, timeout_at, Instant}; diff --git a/tokio/tests/unwindsafe.rs b/tokio/tests/unwindsafe.rs index 8ab6654295b..5f1db57a4f0 100644 --- a/tokio/tests/unwindsafe.rs +++ b/tokio/tests/unwindsafe.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] // Wasi does not support panic recovery +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::panic::{RefUnwindSafe, UnwindSafe}; @@ -14,6 +24,7 @@ fn join_handle_is_unwind_safe() { } #[test] +#[cfg(any(not(target_os = "emscripten"), feature = "net"))] fn net_types_are_unwind_safe() { is_unwind_safe::(); is_unwind_safe::(); @@ -22,8 +33,10 @@ fn net_types_are_unwind_safe() { } #[test] -#[cfg(unix)] +#[cfg(all(unix, any(not(target_os = "emscripten"), feature = "net")))] fn unix_net_types_are_unwind_safe() { + // No datagram `AF_UNIX` on emscripten's node backend. + #[cfg(not(target_os = "emscripten"))] is_unwind_safe::(); is_unwind_safe::(); is_unwind_safe::();