├── .github ├── FUNDING.yaml ├── docsrs-error-nightly.txt ├── docsrs-error-stable.txt ├── docsrs-error-1.60.txt ├── dependabot.yaml ├── workflows │ ├── audit.yaml │ ├── publish.yaml │ ├── spellcheck.yaml │ ├── format.yaml │ ├── upstream.yaml │ ├── merge.yaml │ ├── test.yaml │ ├── lint.yaml │ ├── coverage-documentation.yaml │ └── build.yaml └── upstream-sources.json ├── tests-crates ├── resolver │ ├── .gitignore │ ├── src │ │ └── lib.rs │ └── Cargo.toml └── minimal-versions │ ├── .gitignore │ ├── src │ └── lib.rs │ └── Cargo.toml ├── benches ├── .gitignore ├── index.html └── benchmark.rs ├── .gitignore ├── .cargo └── config.toml ├── taplo.toml ├── .config ├── spellcheck.toml └── topic.dic ├── rustfmt.toml ├── tests ├── util │ ├── std.rs │ ├── mod.rs │ └── web.rs ├── instant_failure_2.rs ├── system_time_failure_2.rs ├── web.rs ├── atomic_failure.rs ├── system_time_failure_1.rs ├── instant_failure_1.rs ├── traits.rs ├── atomic_success.rs ├── system_time_success.rs ├── instant_success.rs └── serde.rs ├── .prettierrc.toml ├── tests-native ├── src │ └── lib.rs └── Cargo.toml ├── clippy.toml ├── src ├── time │ ├── mod.rs │ ├── js.rs │ ├── serde.rs │ ├── system_time.rs │ └── instant.rs ├── web.rs └── lib.rs ├── LICENSE-MIT ├── CHANGELOG.md ├── tests-web ├── Cargo.toml └── src │ └── lib.rs ├── Cargo.toml ├── README.md ├── CONTRIBUTING.md └── LICENSE-APACHE /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: [daxpedda] 2 | -------------------------------------------------------------------------------- /tests-crates/resolver/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /tests-crates/minimal-versions/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /benches/.gitignore: -------------------------------------------------------------------------------- 1 | /snippets 2 | /benchmark_bg.wasm 3 | /benchmark.js 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /coverage-input 4 | /coverage-output 5 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(target_arch = "wasm32")'] 2 | runner = "wasm-bindgen-test-runner" 3 | -------------------------------------------------------------------------------- /.github/docsrs-error-nightly.txt: -------------------------------------------------------------------------------- 1 | `--cfg docsrs` must only be used via `RUSTDOCFLAGS`, not `RUSTFLAGS` 2 | -------------------------------------------------------------------------------- /.github/docsrs-error-stable.txt: -------------------------------------------------------------------------------- 1 | `--cfg docsrs` must only be used via `RUSTDOCFLAGS`, not `RUSTFLAGS` 2 | -------------------------------------------------------------------------------- /taplo.toml: -------------------------------------------------------------------------------- 1 | [formatting] 2 | allowed_blank_lines = 1 3 | column_width = 100 4 | indent_string = ' ' 5 | reorder_keys = true 6 | -------------------------------------------------------------------------------- /tests-crates/resolver/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Testing feature resolver version 1. 2 | 3 | #![cfg_attr(not(feature = "std"), no_std)] 4 | -------------------------------------------------------------------------------- /.github/docsrs-error-1.60.txt: -------------------------------------------------------------------------------- 1 | `--cfg docsrs` must only be used via `RUSTDOCFLAGS`, not `RUSTFLAGS` 2 | aborting due to previous error 3 | -------------------------------------------------------------------------------- /tests-crates/minimal-versions/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Testing minimal versions of dependencies. 2 | 3 | #![cfg_attr(not(feature = "std"), no_std)] 4 | -------------------------------------------------------------------------------- /.config/spellcheck.toml: -------------------------------------------------------------------------------- 1 | dev_comments = true 2 | 3 | [Hunspell] 4 | extra_dictionaries = ["topic.dic"] 5 | search_dirs = ["."] 6 | skip_os_lookups = true 7 | use_builtin = true 8 | -------------------------------------------------------------------------------- /benches/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | format_code_in_doc_comments = true 2 | format_strings = true 3 | group_imports = "StdExternalCrate" 4 | hard_tabs = true 5 | imports_granularity = "Module" 6 | newline_style = "Unix" 7 | unstable_features = true 8 | wrap_comments = true 9 | -------------------------------------------------------------------------------- /.config/topic.dic: -------------------------------------------------------------------------------- 1 | 27 2 | 1G 3 | 1M 4 | 1ns 5 | allocator 6 | APIs 7 | Atomics 8 | de 9 | Changelog 10 | CHANGELOG 11 | Emscripten 12 | io 13 | JS 14 | MDN 15 | MSRV 16 | MSRVs 17 | representable 18 | Serde 19 | Serde's 20 | timestamps 21 | v1 22 | Versioning 23 | WASI 24 | Wasm 25 | WebAssembly 26 | workflow 27 | worklet 28 | + 29 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: cargo 5 | directory: / 6 | schedule: 7 | interval: daily 8 | 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: daily 13 | groups: 14 | github-actions: 15 | patterns: 16 | - "*" 17 | -------------------------------------------------------------------------------- /tests/util/std.rs: -------------------------------------------------------------------------------- 1 | use std::future::{self, Ready}; 2 | use std::thread; 3 | 4 | use web_time::Duration; 5 | 6 | /// Sleeps for the given [`Duration`]. 7 | #[allow(clippy::allow_attributes, dead_code, reason = "not used by all tests")] 8 | pub(crate) fn sleep(duration: Duration) -> Ready<()> { 9 | thread::sleep(duration); 10 | future::ready(()) 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc.toml: -------------------------------------------------------------------------------- 1 | arrowParens = "avoid" 2 | printWidth = 100 3 | proseWrap = "always" 4 | semi = false 5 | singleQuote = true 6 | tabWidth = 4 7 | trailingComma = "es5" 8 | useTabs = true 9 | 10 | # Markdown: https://github.com/prettier/prettier/issues/5019 11 | [[overrides]] 12 | files = ["*.yaml", "*.md"] 13 | options = { singleQuote = false, tabWidth = 2 } 14 | -------------------------------------------------------------------------------- /tests-native/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A crate for running tests on native with the default test harness. 2 | 3 | #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std)] 4 | #![cfg_attr(all(test, target_arch = "wasm32"), no_main)] 5 | 6 | #[cfg(all(test, target_arch = "wasm32"))] 7 | use tests_web as _; 8 | 9 | #[cfg(all(test, not(target_arch = "wasm32")))] 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /tests-crates/minimal-versions/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | edition = "2021" 5 | name = "minimal-versions" 6 | publish = false 7 | rust-version = "1.60" 8 | version = "0.0.0" 9 | 10 | [features] 11 | default = ["web-time/default"] 12 | msrv = ["web-time/msrv"] 13 | serde = ["web-time/serde"] 14 | std = ["web-time/std"] 15 | 16 | [dependencies] 17 | web-time = { path = "../..", default-features = false } 18 | -------------------------------------------------------------------------------- /tests-crates/resolver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | edition = "2021" 5 | name = "resolver" 6 | publish = false 7 | resolver = "1" 8 | rust-version = "1.60" 9 | version = "0.0.0" 10 | 11 | [features] 12 | default = ["web-time/default"] 13 | msrv = ["web-time/msrv"] 14 | serde = ["web-time/serde"] 15 | std = ["web-time/std"] 16 | 17 | [dependencies] 18 | web-time = { path = "../..", default-features = false } 19 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | allow-renamed-params-for = ["..", "core::fmt::Debug", "core::fmt::Display"] 2 | allow-unwrap-in-tests = true 3 | avoid-breaking-exported-api = false 4 | disallowed-methods = [ 5 | { path = "f64::trunc", reason = "not available on `no_std`" }, 6 | { path = "f64::fract", reason = "not available on `no_std`" }, 7 | { path = "f64::copysign", reason = "not available on `no_std`" }, 8 | { path = "f64::round_ties_even", reason = "not available on `no_std` and MSRV" }, 9 | { path = "f64::powi", reason = "not available on `no_std`" }, 10 | ] 11 | semicolon-outside-block-ignore-multiline = true 12 | -------------------------------------------------------------------------------- /.github/workflows/audit.yaml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref_name }} 10 | cancel-in-progress: true 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | audit: 17 | name: Audit 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Install `cargo-audit` 25 | uses: taiki-e/install-action@v2 26 | with: 27 | tool: cargo-audit 28 | - name: Run Audit 29 | run: cargo audit -D warnings 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref_name }} 10 | cancel-in-progress: true 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | publish: 17 | name: Publish 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Install Rust 25 | run: | 26 | rustup toolchain install stable --profile minimal 27 | rustup default stable 28 | - name: Test Publish 29 | run: cargo publish --dry-run 30 | -------------------------------------------------------------------------------- /tests/instant_failure_2.rs: -------------------------------------------------------------------------------- 1 | //! Failure tests have to be separated as `should_panic` can cause serious 2 | //! problems with `panic = "abort"`. 3 | 4 | #![cfg(test)] 5 | #![cfg_attr(target_arch = "wasm32", no_main)] 6 | #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std)] 7 | 8 | mod util; 9 | 10 | use wasm_bindgen_test::wasm_bindgen_test; 11 | use web_time::{Duration, Instant}; 12 | 13 | use self::util::{sleep, WAIT}; 14 | 15 | /// [`Instant::add_assign()`] failure. 16 | #[wasm_bindgen_test(unsupported = pollster::test)] 17 | #[should_panic = "overflow when adding duration to instant"] 18 | async fn add_assign_failure() { 19 | sleep(WAIT).await; 20 | let mut instant = Instant::now(); 21 | instant += Duration::MAX; 22 | } 23 | -------------------------------------------------------------------------------- /tests/system_time_failure_2.rs: -------------------------------------------------------------------------------- 1 | //! Failure tests have to be separated as `should_panic` can cause serious 2 | //! problems with `panic = "abort"`. 3 | 4 | #![cfg(test)] 5 | #![cfg_attr(target_arch = "wasm32", no_main)] 6 | #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std)] 7 | 8 | mod util; 9 | 10 | use wasm_bindgen_test::wasm_bindgen_test; 11 | use web_time::{Duration, SystemTime}; 12 | 13 | use self::util::{sleep, WAIT}; 14 | 15 | /// [`SystemTime::add_assign()`] failure. 16 | #[wasm_bindgen_test(unsupported = pollster::test)] 17 | #[should_panic = "overflow when adding duration to instant"] 18 | async fn add_assign_failure() { 19 | sleep(WAIT).await; 20 | let mut time = SystemTime::now(); 21 | time += Duration::MAX; 22 | } 23 | -------------------------------------------------------------------------------- /src/time/mod.rs: -------------------------------------------------------------------------------- 1 | //! Re-implementation of [`std::time`]. 2 | #![cfg_attr( 3 | not(feature = "std"), 4 | doc = "", 5 | doc = "[`std::time`]: https://doc.rust-lang.org/std/time" 6 | )] 7 | 8 | mod instant; 9 | mod js; 10 | #[cfg(feature = "serde")] 11 | mod serde; 12 | mod system_time; 13 | 14 | #[cfg(not(feature = "std"))] 15 | pub use core::time::*; 16 | #[cfg(feature = "std")] 17 | pub use std::time::*; 18 | 19 | pub use self::instant::Instant; 20 | pub use self::system_time::{SystemTime, SystemTimeError}; 21 | 22 | /// See [`std::time::UNIX_EPOCH`]. 23 | #[cfg_attr( 24 | not(feature = "std"), 25 | doc = "", 26 | doc = "[`std::time::UNIX_EPOCH`]: https://doc.rust-lang.org/std/time/constant.UNIX_EPOCH.html" 27 | )] 28 | pub const UNIX_EPOCH: SystemTime = SystemTime::UNIX_EPOCH; 29 | -------------------------------------------------------------------------------- /.github/upstream-sources.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "owner": "rust-lang", "repo": "libm", "tag": "libm-v0.2.11", "path": "src/math/trunc.rs" }, 3 | { "owner": "rust-lang", "repo": "libm", "tag": "libm-v0.2.11", "path": "src/math/rint.rs" }, 4 | { "owner": "rust-lang", "repo": "rust", "tag": "1.83.0", "path": "library/core/src/time.rs" }, 5 | { "owner": "rust-lang", "repo": "rust", "tag": "1.83.0", "path": "library/std/src/time.rs" }, 6 | { 7 | "owner": "rust-lang", 8 | "repo": "rust", 9 | "tag": "1.83.0", 10 | "path": "library/std/src/sys/pal/unsupported/time.rs" 11 | }, 12 | { 13 | "owner": "rust-lang", 14 | "repo": "rust", 15 | "tag": "1.83.0", 16 | "path": "library/std/src/sys/alloc/wasm.rs" 17 | }, 18 | { 19 | "owner": "serde-rs", 20 | "repo": "serde", 21 | "tag": "v1.0.215", 22 | "path": "serde/src/de/impls.rs" 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 dAxpeDDa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/web.rs: -------------------------------------------------------------------------------- 1 | //! Test Web-specific API exported in [`web_time::web`]. 2 | 3 | #![cfg(test)] 4 | 5 | mod util; 6 | 7 | use std::time; 8 | use std::time::Duration; 9 | 10 | use wasm_bindgen_test::wasm_bindgen_test; 11 | use web_time::web::SystemTimeExt; 12 | 13 | /// Testing conversion from [`web_time`] to [`std`]. 14 | #[wasm_bindgen_test] 15 | fn to_std() { 16 | assert_eq!( 17 | web_time::SystemTime::UNIX_EPOCH.to_std(), 18 | time::SystemTime::UNIX_EPOCH, 19 | ); 20 | 21 | let duration = Duration::from_secs(60 * 60 * 24 * 365); 22 | assert_eq!( 23 | (web_time::SystemTime::UNIX_EPOCH + duration).to_std(), 24 | time::SystemTime::UNIX_EPOCH + duration, 25 | ); 26 | } 27 | 28 | /// Testing conversion from [`std`] to [`web_time`]. 29 | #[wasm_bindgen_test] 30 | fn from_std() { 31 | assert_eq!( 32 | web_time::SystemTime::from_std(time::SystemTime::UNIX_EPOCH), 33 | web_time::SystemTime::UNIX_EPOCH, 34 | ); 35 | 36 | let duration = Duration::from_secs(60 * 60 * 24 * 365); 37 | assert_eq!( 38 | web_time::SystemTime::from_std(time::SystemTime::UNIX_EPOCH + duration), 39 | web_time::SystemTime::UNIX_EPOCH + duration, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/spellcheck.yaml: -------------------------------------------------------------------------------- 1 | name: Spellcheck 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref_name }} 10 | cancel-in-progress: true 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | spellcheck: 17 | name: Spellcheck 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Install `cargo-spellcheck` 25 | uses: taiki-e/install-action@v2 26 | with: 27 | tool: cargo-spellcheck 28 | - name: Run Spellcheck 29 | run: | 30 | cargo spellcheck check -m 1 31 | cargo spellcheck check -m 1 CHANGELOG.md 32 | cargo spellcheck check -m 1 CONTRIBUTING.md 33 | 34 | typos: 35 | name: Typos 36 | 37 | runs-on: ubuntu-latest 38 | 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | - name: Install Typos 43 | uses: taiki-e/install-action@v2 44 | with: 45 | tool: typos-cli 46 | - name: Run Typos 47 | run: typos 48 | -------------------------------------------------------------------------------- /tests/util/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utility types and functions. 2 | 3 | #[cfg(not(target_arch = "wasm32"))] 4 | mod std; 5 | #[cfg(target_arch = "wasm32")] 6 | mod web; 7 | 8 | #[cfg(target_arch = "wasm32")] 9 | use tests_web as _; 10 | use web_time::Duration; 11 | 12 | #[cfg(not(target_arch = "wasm32"))] 13 | #[allow( 14 | clippy::allow_attributes, 15 | unused_imports, 16 | reason = "not used by all tests" 17 | )] 18 | pub(crate) use self::std::*; 19 | #[cfg(target_arch = "wasm32")] 20 | #[allow( 21 | clippy::allow_attributes, 22 | unused_imports, 23 | reason = "not used by all tests" 24 | )] 25 | pub(crate) use self::web::*; 26 | 27 | /// Time to wait. 28 | pub(crate) const WAIT: Duration = Duration::from_millis(50); 29 | /// Difference to measure that time has passed. 30 | #[allow(clippy::allow_attributes, dead_code, reason = "not used by all tests")] 31 | pub(crate) const DIFF: Duration = Duration::from_millis(49); 32 | /// Maximum difference that can't have been passed by [`DIFF`]. 33 | #[allow(clippy::allow_attributes, dead_code, reason = "not used by all tests")] 34 | pub(crate) const MAX_DIFF: Duration = if let Some(duration) = WAIT.checked_mul(10) { 35 | duration 36 | } else { 37 | panic!() 38 | }; 39 | -------------------------------------------------------------------------------- /tests/atomic_failure.rs: -------------------------------------------------------------------------------- 1 | //! Run tests with the atomics target feature. 2 | 3 | #![cfg(test)] 4 | #![cfg(target_feature = "atomics")] 5 | 6 | mod util; 7 | 8 | use futures_util::future; 9 | use futures_util::future::Either; 10 | use wasm_bindgen_test::wasm_bindgen_test; 11 | use web_sys::{console, OfflineAudioContext}; 12 | use web_thread::web::audio_worklet::BaseAudioContextExt; 13 | use web_time::Instant; 14 | 15 | use self::util::{Flag, MAX_DIFF}; 16 | 17 | /// Testing failure of [`Instant::now()`] in audio worklet. 18 | #[wasm_bindgen_test] 19 | async fn test() { 20 | if web_sys::window().is_none() { 21 | console::error_1(&"found ourselves not in a `Window`".into()); 22 | return; 23 | } 24 | 25 | let context = 26 | OfflineAudioContext::new_with_number_of_channels_and_length_and_sample_rate(1, 1, 8000.) 27 | .unwrap(); 28 | 29 | let flag = Flag::new(); 30 | 31 | context 32 | .clone() 33 | .register_thread(None, { 34 | let flag = flag.clone(); 35 | move || { 36 | let _ = Instant::now(); 37 | flag.signal(); 38 | } 39 | }) 40 | .await 41 | .unwrap(); 42 | 43 | assert!(matches!( 44 | future::select(flag, util::sleep(MAX_DIFF)).await, 45 | Either::Right(_) 46 | )); 47 | } 48 | -------------------------------------------------------------------------------- /tests/system_time_failure_1.rs: -------------------------------------------------------------------------------- 1 | //! Failure tests have to be separated as `should_panic` can cause serious 2 | //! problems with `panic = "abort"`. 3 | 4 | #![cfg(test)] 5 | #![cfg_attr(target_arch = "wasm32", no_main)] 6 | #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std)] 7 | 8 | mod util; 9 | 10 | use wasm_bindgen_test::wasm_bindgen_test; 11 | use web_time::{Duration, SystemTime}; 12 | 13 | use self::util::{sleep, WAIT}; 14 | 15 | /// [`SystemTime::add()`] failure. 16 | #[wasm_bindgen_test(unsupported = pollster::test)] 17 | #[should_panic = "overflow when adding duration to instant"] 18 | async fn add_failure() { 19 | sleep(WAIT).await; 20 | let _ = SystemTime::now() + Duration::MAX; 21 | } 22 | 23 | /// [`SystemTime::sub()`] failure. 24 | #[wasm_bindgen_test(unsupported = test)] 25 | #[should_panic = "overflow when subtracting duration from instant"] 26 | fn sub_failure() { 27 | let _ = SystemTime::now() - Duration::MAX; 28 | } 29 | 30 | /// [`SystemTime::sub_assign()`] failure. 31 | #[wasm_bindgen_test(unsupported = test)] 32 | #[should_panic = "overflow when subtracting duration from instant"] 33 | fn sub_assign_failure() { 34 | let time = SystemTime::now(); 35 | let _ = time - Duration::MAX; 36 | } 37 | -------------------------------------------------------------------------------- /tests/instant_failure_1.rs: -------------------------------------------------------------------------------- 1 | //! Failure tests have to be separated as `should_panic` can cause serious 2 | //! problems with `panic = "abort"`. 3 | 4 | #![cfg(test)] 5 | #![cfg_attr(target_arch = "wasm32", no_main)] 6 | #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std)] 7 | 8 | mod util; 9 | 10 | use wasm_bindgen_test::wasm_bindgen_test; 11 | use web_time::{Duration, Instant}; 12 | 13 | use self::util::{sleep, WAIT}; 14 | 15 | /// [`Instant::add()`] failure. 16 | #[wasm_bindgen_test(unsupported = pollster::test)] 17 | #[should_panic = "overflow when adding duration to instant"] 18 | async fn add_failure() { 19 | sleep(WAIT).await; 20 | let _ = Instant::now() + Duration::MAX; 21 | } 22 | 23 | /// [`Instant::sub()`] failure. 24 | #[wasm_bindgen_test(unsupported = test)] 25 | #[should_panic = "overflow when subtracting duration from instant"] 26 | #[allow( 27 | clippy::allow_attributes, 28 | clippy::unchecked_duration_subtraction, 29 | reason = "this is what we are testing" 30 | )] 31 | fn sub_failure() { 32 | let _ = Instant::now() - Duration::MAX; 33 | } 34 | 35 | /// [`Instant::sub_assign()`] failure. 36 | #[wasm_bindgen_test(unsupported = test)] 37 | #[should_panic = "overflow when subtracting duration from instant"] 38 | fn sub_assign_failure() { 39 | let mut instant = Instant::now(); 40 | instant -= Duration::MAX; 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/format.yaml: -------------------------------------------------------------------------------- 1 | name: Format 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref_name }} 10 | cancel-in-progress: true 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | rustfmt: 17 | name: Rustfmt 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Install Rust nightly 25 | run: | 26 | rustup toolchain install nightly --profile minimal --component rustfmt --allow-downgrade 27 | rustup default nightly 28 | - name: Run Rustfmt 29 | run: cargo fmt --check 30 | 31 | taplo: 32 | name: Taplo 33 | 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | - name: Install Taplo 40 | uses: taiki-e/install-action@v2 41 | with: 42 | tool: taplo-cli 43 | - name: Run Taplo 44 | run: taplo fmt --check 45 | 46 | prettier: 47 | name: Prettier 48 | 49 | runs-on: ubuntu-latest 50 | 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | - name: Install Prettier 55 | run: sudo npm i -g prettier 56 | - name: Run Prettier 57 | run: prettier . --check 58 | -------------------------------------------------------------------------------- /src/time/js.rs: -------------------------------------------------------------------------------- 1 | //! Bindings to the JS API. 2 | 3 | use wasm_bindgen::prelude::wasm_bindgen; 4 | 5 | #[wasm_bindgen] 6 | extern "C" { 7 | /// Type for the [`Performance` object](https://developer.mozilla.org/en-US/docs/Web/API/Performance). 8 | pub(super) type Performance; 9 | 10 | /// Holds the [`Performance`](https://developer.mozilla.org/en-US/docs/Web/API/Performance) object. 11 | #[wasm_bindgen(thread_local_v2, js_namespace = globalThis, js_name = performance)] 12 | pub(super) static PERFORMANCE: Option; 13 | 14 | /// Binding to [`Performance.now()`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now). 15 | #[wasm_bindgen(method)] 16 | pub(super) fn now(this: &Performance) -> f64; 17 | 18 | /// Holds the [`Performance.timeOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin). 19 | #[cfg(target_feature = "atomics")] 20 | #[wasm_bindgen(thread_local_v2, js_namespace = ["globalThis", "performance"], js_name = timeOrigin)] 21 | pub(super) static TIME_ORIGIN: f64; 22 | 23 | /// Type for the [`Date` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). 24 | pub(super) type Date; 25 | 26 | /// Binding to [`Date.now()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now). 27 | #[wasm_bindgen(static_method_of = Date)] 28 | pub(super) fn now() -> f64; 29 | } 30 | -------------------------------------------------------------------------------- /tests/traits.rs: -------------------------------------------------------------------------------- 1 | //! Test for traits on all exported types. 2 | 3 | #![cfg(test)] 4 | #![cfg_attr(target_arch = "wasm32", no_main)] 5 | #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std)] 6 | 7 | mod util; 8 | 9 | use core::fmt::{Debug, Display}; 10 | use core::hash::Hash; 11 | use core::ops::{Add, AddAssign, Sub, SubAssign}; 12 | use core::panic::{RefUnwindSafe, UnwindSafe}; 13 | #[cfg(feature = "std")] 14 | use std::error::Error; 15 | 16 | use static_assertions::{assert_impl_all, assert_not_impl_any}; 17 | use web_time::{Duration, Instant, SystemTime, SystemTimeError}; 18 | 19 | /// Testing all traits on all types. 20 | #[wasm_bindgen_test::wasm_bindgen_test(unsupported = test)] 21 | const fn test() { 22 | assert_impl_all!(Instant: Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Send, Sync, Unpin, RefUnwindSafe, UnwindSafe); 23 | assert_impl_all!(Instant: Add, AddAssign, Sub, SubAssign); 24 | 25 | assert_impl_all!(SystemTime: Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Send, Sync, Unpin, RefUnwindSafe, UnwindSafe); 26 | assert_impl_all!(SystemTime: Add, AddAssign, Sub, SubAssign); 27 | 28 | assert_impl_all!(SystemTimeError: Clone, Debug, Display, Send, Sync, Unpin, RefUnwindSafe, UnwindSafe); 29 | #[cfg(feature = "std")] 30 | assert_impl_all!(SystemTimeError: Error); 31 | assert_not_impl_any!(SystemTimeError: Copy, Hash, Eq, PartialEq, Ord, PartialOrd); 32 | } 33 | -------------------------------------------------------------------------------- /tests-native/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "tests-native" 4 | publish = false 5 | version = "0.0.0" 6 | 7 | [features] 8 | default = ["std"] 9 | run = [] 10 | std = ["tests-web/std", "web-time/std"] 11 | 12 | [target.'cfg(target_arch = "wasm32")'.dependencies] 13 | tests-web = { path = "../tests-web", default-features = false } 14 | 15 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 16 | pollster = { version = "0.4", features = ["macro"] } 17 | serde-json-core = { version = "0.6", default-features = false, features = ["std"] } 18 | serde_json = "1" 19 | serde_test = "1" 20 | static_assertions = "1" 21 | wasm-bindgen-test = { version = "0.3" } 22 | web-time = { path = "../", default-features = false } 23 | 24 | [lib] 25 | bench = false 26 | doctest = false 27 | harness = false 28 | test = false 29 | 30 | [[test]] 31 | name = "native_instant_failure_1" 32 | path = "../tests/instant_failure_1.rs" 33 | required-features = ["run"] 34 | 35 | [[test]] 36 | name = "native_instant_failure_2" 37 | path = "../tests/instant_failure_2.rs" 38 | required-features = ["run"] 39 | 40 | [[test]] 41 | name = "native_instant_success" 42 | path = "../tests/instant_success.rs" 43 | required-features = ["run"] 44 | 45 | [[test]] 46 | name = "native_serde" 47 | path = "../tests/serde.rs" 48 | required-features = ["run"] 49 | 50 | [[test]] 51 | name = "native_system_time_failure_1" 52 | path = "../tests/system_time_failure_1.rs" 53 | required-features = ["run"] 54 | 55 | [[test]] 56 | name = "native_system_time_failure_2" 57 | path = "../tests/system_time_failure_2.rs" 58 | required-features = ["run"] 59 | 60 | [[test]] 61 | name = "native_system_time_success" 62 | path = "../tests/system_time_success.rs" 63 | required-features = ["run"] 64 | 65 | [[test]] 66 | name = "traits" 67 | path = "../tests/traits.rs" 68 | required-features = ["run"] 69 | 70 | [lints] 71 | workspace = true 72 | -------------------------------------------------------------------------------- /.github/workflows/upstream.yaml: -------------------------------------------------------------------------------- 1 | name: Upstream Tracking 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | schedule: 8 | - cron: "0 7 * * *" 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref_name }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | upstream: 16 | name: Upstream Tracking 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Find if any changes occured upstream 24 | uses: actions/github-script@v7 25 | with: 26 | script: | 27 | const upstreamSources = require('./.github/upstream-sources.json') 28 | for (const source of upstreamSources) { 29 | Promise.all([ 30 | github.rest.repos.getContent({ 31 | owner: source.owner, 32 | repo: source.repo, 33 | path: source.path, 34 | ref: source.tag, 35 | }), 36 | github.rest.repos 37 | .getLatestRelease({ 38 | owner: source.owner, 39 | repo: source.repo, 40 | }) 41 | .then((tags) => { 42 | return github.rest.repos.getContent({ 43 | owner: source.owner, 44 | repo: source.repo, 45 | path: source.path, 46 | ref: tags.data.tag_name, 47 | }) 48 | }), 49 | ]).then(([current, latest]) => { 50 | if (current.data.sha != latest.data.sha) { 51 | core.setFailed(` 52 | <${current.data.html_url}> has been updated to <${latest.data.html_url}>. 53 | Check if the relevant code needs to be updated and then update the entry in \`.github/upstream-sources.json\`. 54 | `) 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /tests/atomic_success.rs: -------------------------------------------------------------------------------- 1 | //! Run tests with the atomics target feature. 2 | 3 | #![cfg(test)] 4 | #![cfg(target_feature = "atomics")] 5 | 6 | mod util; 7 | 8 | use futures_channel::oneshot; 9 | use wasm_bindgen_test::wasm_bindgen_test; 10 | use web_sys::console; 11 | use web_thread::web::{self, has_spawn_support}; 12 | use web_time::{Duration, Instant}; 13 | 14 | use self::util::{sleep, Flag, DIFF, WAIT}; 15 | 16 | #[wasm_bindgen_test] 17 | async fn basic() { 18 | if !has_spawn_support() { 19 | console::error_1(&"can't spawn threads".into()); 20 | return; 21 | } 22 | 23 | let earlier = Instant::now(); 24 | 25 | let flag = Flag::new(); 26 | 27 | web::spawn_async({ 28 | let flag = flag.clone(); 29 | move || async move { 30 | let later = Instant::now(); 31 | assert!(earlier <= later, "{:?}", earlier - later); 32 | 33 | sleep(WAIT).await; 34 | 35 | let later = Instant::now(); 36 | assert!((later - earlier) >= DIFF, "{:?}", later - earlier); 37 | 38 | let later = Instant::now(); 39 | assert!(earlier <= later, "{:?}", earlier - later); 40 | 41 | flag.signal(); 42 | } 43 | }); 44 | 45 | flag.await; 46 | } 47 | 48 | #[wasm_bindgen_test] 49 | async fn delay() { 50 | if !has_spawn_support() { 51 | console::error_1(&"can't spawn threads".into()); 52 | return; 53 | } 54 | 55 | sleep(Duration::from_secs(2)).await; 56 | 57 | let earlier = Instant::now(); 58 | 59 | let flag = Flag::new(); 60 | 61 | web::spawn_async({ 62 | let flag = flag.clone(); 63 | move || async move { 64 | let later = Instant::now(); 65 | assert!(earlier <= later, "{:?}", earlier - later); 66 | 67 | sleep(WAIT).await; 68 | 69 | let later = Instant::now(); 70 | assert!((later - earlier) >= DIFF, "{:?}", later - earlier); 71 | 72 | let later = Instant::now(); 73 | assert!(earlier <= later, "{:?}", earlier - later); 74 | 75 | flag.signal(); 76 | } 77 | }); 78 | 79 | flag.await; 80 | } 81 | 82 | #[wasm_bindgen_test] 83 | async fn worker() { 84 | if !has_spawn_support() { 85 | console::error_1(&"can't spawn threads".into()); 86 | return; 87 | } 88 | 89 | let (sender, receiver) = oneshot::channel(); 90 | web::spawn_async(move || async move { sender.send(Instant::now()).unwrap() }); 91 | 92 | let earlier = receiver.await.unwrap(); 93 | let later = Instant::now(); 94 | assert!(earlier <= later, "{:?}", earlier - later); 95 | 96 | sleep(WAIT).await; 97 | 98 | let later = Instant::now(); 99 | assert!((later - earlier) >= DIFF, "{:?}", later - earlier); 100 | 101 | let later = Instant::now(); 102 | assert!(earlier <= later, "{:?}", earlier - later); 103 | } 104 | -------------------------------------------------------------------------------- /src/web.rs: -------------------------------------------------------------------------------- 1 | //! Platform-specific extensions to [`web-time`](crate) for the Web platform. 2 | 3 | #![allow(clippy::absolute_paths)] 4 | 5 | use std::time::SystemTime as StdSystemTime; 6 | 7 | use crate::SystemTime; 8 | 9 | #[cfg(all( 10 | target_arch = "wasm32", 11 | any(target_os = "unknown", target_os = "none"), 12 | not(feature = "std"), 13 | ))] 14 | #[doc(hidden)] 15 | mod std { 16 | pub mod time { 17 | pub struct SystemTime; 18 | } 19 | } 20 | 21 | /// Web-specific extension to [`web_time::SystemTime`](crate::SystemTime). 22 | pub trait SystemTimeExt { 23 | /// Convert [`web_time::SystemTime`](crate::SystemTime) to 24 | /// [`std::time::SystemTime`]. 25 | /// 26 | /// # Note 27 | /// 28 | /// This might give a misleading impression of compatibility! 29 | /// 30 | /// Considering this functionality will probably be used to interact with 31 | /// incompatible APIs of other dependencies, care should be taken that the 32 | /// dependency in question doesn't call [`std::time::SystemTime::now()`] 33 | /// internally, which would panic. 34 | #[cfg_attr( 35 | all( 36 | target_arch = "wasm32", 37 | any(target_os = "unknown", target_os = "none"), 38 | not(feature = "std"), 39 | ), 40 | doc = "", 41 | doc = "[`std::time::SystemTime`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html", 42 | doc = "[`std::time::SystemTime::now()`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html#method.now" 43 | )] 44 | fn to_std(self) -> std::time::SystemTime; 45 | 46 | /// Convert [`std::time::SystemTime`] to 47 | /// [`web_time::SystemTime`](crate::SystemTime). 48 | /// 49 | /// # Note 50 | /// 51 | /// This might give a misleading impression of compatibility! 52 | /// 53 | /// Considering this functionality will probably be used to interact with 54 | /// incompatible APIs of other dependencies, care should be taken that the 55 | /// dependency in question doesn't call [`std::time::SystemTime::now()`] 56 | /// internally, which would panic. 57 | #[cfg_attr( 58 | all( 59 | target_arch = "wasm32", 60 | any(target_os = "unknown", target_os = "none"), 61 | not(feature = "std"), 62 | ), 63 | doc = "", 64 | doc = "[`std::time::SystemTime`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html", 65 | doc = "[`std::time::SystemTime::now()`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html#method.now" 66 | )] 67 | fn from_std(time: std::time::SystemTime) -> SystemTime; 68 | } 69 | 70 | impl SystemTimeExt for SystemTime { 71 | fn to_std(self) -> std::time::SystemTime { 72 | StdSystemTime::UNIX_EPOCH + self.0 73 | } 74 | 75 | fn from_std(time: std::time::SystemTime) -> SystemTime { 76 | Self::UNIX_EPOCH 77 | + time 78 | .duration_since(StdSystemTime::UNIX_EPOCH) 79 | .expect("found `SystemTime` earlier than unix epoch") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/merge.yaml: -------------------------------------------------------------------------------- 1 | name: Fast-forward merge 2 | 3 | on: 4 | issue_comment: 5 | types: [created, edited] 6 | 7 | jobs: 8 | fast_forward: 9 | name: Fast-forward merge 10 | 11 | runs-on: ubuntu-latest 12 | 13 | if: | 14 | github.event.issue.pull_request && 15 | github.event.issue.state == 'open' && 16 | ( 17 | github.event.comment.author_association == 'OWNER' || 18 | github.event.comment.author_association == 'MEMBER' || 19 | github.event.comment.author_association == 'COLLABORATOR' 20 | ) && 21 | github.event.comment.body == '/fast-forward-merge' 22 | 23 | permissions: 24 | actions: write 25 | contents: write 26 | pull-requests: write 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | - name: Minimize calling comment 32 | uses: actions/github-script@v7 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | script: | 36 | await github.graphql(`mutation { minimizeComment(input: { subjectId: "${context.payload.comment.node_id}", classifier: OUTDATED }) { clientMutationId } }`) 37 | const { data: comments } = await github.rest.issues.listComments({ 38 | ...context.repo, 39 | issue_number: ${{ github.event.issue.number }}, 40 | }); 41 | for (const comment of comments) { 42 | if (comment.user.login === 'github-actions[bot]' && comment.body.startsWith('Failure merging:')) { 43 | await github.graphql(`mutation { minimizeComment(input: { subjectId: "${comment.node_id}", classifier: OUTDATED }) { clientMutationId } }`) 44 | } 45 | } 46 | - name: Checkout pull request 47 | id: pr 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | run: | 51 | gh pr checkout ${{ github.event.issue.number }} 52 | echo "base_ref=$(gh pr view ${{ github.event.issue.number }} --json baseRefName)" >> $GITHUB_OUTPUT 53 | - name: Fast-forward merge 54 | run: | 55 | export PR_COMMIT=$(git rev-parse HEAD) 56 | git checkout ${{ fromJSON(steps.pr.outputs.base_ref).baseRefName }} 57 | git merge --ff-only "$PR_COMMIT" 2>output.log 58 | git push origin ${{ fromJSON(steps.pr.outputs.base_ref).baseRefName }} 2>output.log 59 | - name: Post errors 60 | if: ${{ failure() }} 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | run: | 64 | gh pr comment ${{ github.event.issue.number }} -b "Failure merging: 65 | \`\`\` 66 | $(cat output.log) 67 | \`\`\`" 68 | - name: Run CI 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | run: | 72 | gh workflow run "Coverage & Documentation" -r ${{ fromJSON(steps.pr.outputs.base_ref).baseRefName }} 73 | -------------------------------------------------------------------------------- /tests/util/web.rs: -------------------------------------------------------------------------------- 1 | //! Web specific utility. 2 | 3 | #[cfg(all(target_feature = "atomics", feature = "std"))] 4 | extern crate alloc; 5 | 6 | #[cfg(all(target_feature = "atomics", feature = "std"))] 7 | use alloc::sync::Arc; 8 | use core::future::Future; 9 | use core::pin::Pin; 10 | #[cfg(all(target_feature = "atomics", feature = "std"))] 11 | use core::sync::atomic::{AtomicBool, Ordering}; 12 | use core::task::{ready, Context, Poll}; 13 | use core::time::Duration; 14 | 15 | #[cfg(all(target_feature = "atomics", feature = "std"))] 16 | use futures_util::task::AtomicWaker; 17 | use js_sys::{Function, Promise}; 18 | use wasm_bindgen::prelude::wasm_bindgen; 19 | use wasm_bindgen_futures::JsFuture; 20 | 21 | /// Async version of [`std::thread::sleep()`]. 22 | pub(crate) struct Sleep(JsFuture); 23 | 24 | impl Future for Sleep { 25 | type Output = (); 26 | 27 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 28 | ready!(Pin::new(&mut self.0).poll(cx)).unwrap(); 29 | Poll::Ready(()) 30 | } 31 | } 32 | 33 | /// Sleeps for the given [`Duration`]. 34 | #[allow(clippy::allow_attributes, dead_code, reason = "not used by all tests")] 35 | pub(crate) fn sleep(duration: Duration) -> Sleep { 36 | #[wasm_bindgen] 37 | extern "C" { 38 | #[wasm_bindgen(js_name = setTimeout)] 39 | fn set_timeout(handler: &Function, timeout: i32); 40 | } 41 | 42 | let future = JsFuture::from(Promise::new(&mut |resolve, _| { 43 | let duration = duration.as_millis().try_into().unwrap(); 44 | set_timeout(&resolve, duration); 45 | })); 46 | 47 | Sleep(future) 48 | } 49 | 50 | /// Can be awaited to wake up thread when signaled. 51 | #[cfg(all(target_feature = "atomics", feature = "std"))] 52 | #[derive(Clone)] 53 | pub(crate) struct Flag(Arc); 54 | 55 | /// Shared data for [`Flag`]. 56 | #[cfg(all(target_feature = "atomics", feature = "std"))] 57 | struct Inner { 58 | /// The registered thread to wake. 59 | waker: AtomicWaker, 60 | /// If the [`Flag`] was [`signal()`](Flag::signal)ed. 61 | set: AtomicBool, 62 | } 63 | 64 | #[cfg(all(target_feature = "atomics", feature = "std"))] 65 | #[allow(clippy::allow_attributes, dead_code, reason = "not used by all tests")] 66 | impl Flag { 67 | /// Creates a new [`Flag`]. 68 | pub(crate) fn new() -> Self { 69 | Self(Arc::new(Inner { 70 | waker: AtomicWaker::new(), 71 | set: AtomicBool::new(false), 72 | })) 73 | } 74 | 75 | /// Will wake up any thread waiting on this [`Flag`]. 76 | /// 77 | /// Any thread awaiting this [`Flag`] will wake up immediately. 78 | pub(crate) fn signal(&self) { 79 | self.0.set.store(true, Ordering::Relaxed); 80 | self.0.waker.wake(); 81 | } 82 | } 83 | 84 | #[cfg(all(target_feature = "atomics", feature = "std"))] 85 | impl Future for Flag { 86 | type Output = (); 87 | 88 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { 89 | // Short-circuit. 90 | if self.0.set.load(Ordering::Relaxed) { 91 | return Poll::Ready(()); 92 | } 93 | 94 | self.0.waker.register(cx.waker()); 95 | 96 | if self.0.set.load(Ordering::Relaxed) { 97 | Poll::Ready(()) 98 | } else { 99 | Poll::Pending 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project 6 | adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - [`no_std`] support through a `std` crate feature. 13 | - Support for the [`wasm32v1-none`] target. 14 | - MSRV policy. 15 | - A `msrv` crate feature that allows `web-time` to make use of features only available in higher 16 | MSRVs. This offers compile-time detection and does not break compilation when enabled with the 17 | crates MSRV. 18 | 19 | - Rust v1.77 + `std`: Enables the use of the [`f64.nearest`] instruction. Which will significantly 20 | reduce the instruction count for `Instant::now()`. 21 | - Rust Nightly: Enables the use of the [`f64.trunc`] and [`f64.nearest`] instruction. Which will 22 | significantly reduce the instruction count for `Instant::now()`. 23 | 24 | ### Changed 25 | 26 | - Improve performance of `Instant::now()` by using `f64::round_ties_even()` instead of 27 | `f64::round()` internally. 28 | - Removed `js-sys` dependency in favor of custom bindings. 29 | 30 | ### Fixed 31 | 32 | - As `wasm64-*` is not supported, `web-time` now falls back to `std` instead of unnecessarily 33 | pulling in dependencies on these targets. 34 | 35 | [`no_std`]: https://doc.rust-lang.org/1.82.0/reference/names/preludes.html#the-no_std-attribute 36 | [`wasm32v1-none`]: https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32v1-none.html 37 | [`f64.nearest`]: 38 | https://webassembly.github.io/spec/core/syntax/instructions.html#syntax-instr-numeric 39 | [`f64.trunc`]: https://webassembly.github.io/spec/core/syntax/instructions.html#syntax-instr-numeric 40 | 41 | ## [1.1.0] - 2024-03-01 42 | 43 | ### Added 44 | 45 | - Serde de/serialization support for `SystemTime` through a `serde` crate feature. 46 | 47 | ## [1.0.0] - 2024-01-14 48 | 49 | ### Added 50 | 51 | - V1 release. 52 | 53 | ## [0.2.4] - 2023-12-24 54 | 55 | ### Added 56 | 57 | - `web` module containing a platform-specific extension trait to `SystemTime`, allowing conversion 58 | from and to `std::time::SystemTime`. 59 | 60 | ### Changed 61 | 62 | - Improve performance of `SystemTime` by using `Duration` internally. 63 | 64 | ## [0.2.3] - 2023-10-23 65 | 66 | ### Changed 67 | 68 | - Improve accuracy of `Instant::now()`. 69 | 70 | ## [0.2.2] - 2023-10-08 71 | 72 | ### Fixed 73 | 74 | - Time conversion for `Instant`. 75 | 76 | ## [0.2.1] - 2023-10-07 [YANKED] 77 | 78 | ### Changed 79 | 80 | - Bump MSRV to v1.60. 81 | 82 | ### Removed 83 | 84 | - Unnecessary `once_cell` dependency. 85 | 86 | ## [0.2.0] - 2023-03-28 87 | 88 | ### Added 89 | 90 | - Export [`TryFromFloatSecsError`] without breaking MSRV. 91 | 92 | [`TryFromFloatSecsError`]: https://doc.rust-lang.org/std/time/struct.TryFromFloatSecsError.html 93 | 94 | ## [0.1.0] - 2023-03-27 95 | 96 | ### Added 97 | 98 | - Initial release. 99 | 100 | [Unreleased]: https://github.com/daxpedda/web-time/compare/v1.1.0...HEAD 101 | [1.1.0]: https://github.com/daxpedda/web-time/compare/v1.0.0...v1.1.0 102 | [1.0.0]: https://github.com/daxpedda/web-time/compare/v0.2.4...v1.0.0 103 | [0.2.4]: https://github.com/daxpedda/web-time/compare/v0.2.3...v0.2.4 104 | [0.2.3]: https://github.com/daxpedda/web-time/compare/v0.2.2...v0.2.3 105 | [0.2.2]: https://github.com/daxpedda/web-time/compare/v0.2.1...v0.2.2 106 | [0.2.1]: https://github.com/daxpedda/web-time/compare/v0.2.0...v0.2.1 107 | [0.2.0]: https://github.com/daxpedda/web-time/compare/v0.1.0...v0.2.0 108 | [0.1.0]: https://github.com/daxpedda/web-time/releases/tag/v0.1.0 109 | -------------------------------------------------------------------------------- /tests-web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "tests-web" 4 | publish = false 5 | version = "0.0.0" 6 | 7 | [features] 8 | default = ["std"] 9 | run = [] 10 | serde = ["serde_test", "serde_json", "serde-json-core"] 11 | std = [ 12 | "wasm-bindgen/std", 13 | "js-sys/std", 14 | "web-sys/std", 15 | "wasm-bindgen-futures/std", 16 | "wasm-bindgen-test/std", 17 | "serde_test?/std", 18 | "serde_json?/std", 19 | "serde-json-core?/std", 20 | "getrandom/std", 21 | "rand/std", 22 | "futures-util/std", 23 | "futures-channel/std", 24 | "web-thread", 25 | "web-time/std", 26 | ] 27 | 28 | [target.'cfg(target_arch = "wasm32")'.dependencies] 29 | dlmalloc = "0.2" 30 | getrandom = { version = "0.2", features = ["js"] } 31 | js-sys = { version = "0.3", default-features = false } 32 | libm = "0.2" 33 | rand = { version = "0.8", default-features = false, features = ["getrandom", "std_rng"] } 34 | serde-json-core = { version = "0.6", optional = true, default-features = false } 35 | serde_json = { version = "1", optional = true, default-features = false, features = ["alloc"] } 36 | serde_test = { version = "1", optional = true, default-features = false } 37 | static_assertions = "1" 38 | wasm-bindgen = { version = "0.2", default-features = false } 39 | wasm-bindgen-futures = { version = "0.4", default-features = false } 40 | wasm-bindgen-test = { version = "0.3", default-features = false, features = [ 41 | "msrv", 42 | "unstable-test-coverage", 43 | ] } 44 | web-sys = { version = "0.3", default-features = false, features = [ 45 | "CssStyleDeclaration", 46 | "Document", 47 | "Element", 48 | "HtmlTableElement", 49 | "HtmlTableRowElement", 50 | "Performance", 51 | "Window", 52 | ] } 53 | web-time = { path = "../", default-features = false } 54 | 55 | [target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies] 56 | futures-channel = { version = "0.3", default-features = false, features = ["alloc"] } 57 | futures-util = { version = "0.3", default-features = false } 58 | web-sys = { version = "0.3", default-features = false, features = [ 59 | "console", 60 | "OfflineAudioContext", 61 | ] } 62 | web-thread = { git = "https://github.com/daxpedda/wasm-worker", rev = "ce376d95dbdd9e7b59ac7de9c6f14090076f7865", optional = true, features = [ 63 | "audio-worklet", 64 | ] } 65 | 66 | [lib] 67 | bench = false 68 | doctest = false 69 | harness = false 70 | test = false 71 | 72 | [[example]] 73 | harness = false 74 | name = "benchmark" 75 | path = "../benches/benchmark.rs" 76 | required-features = ["run"] 77 | test = false 78 | 79 | [[test]] 80 | name = "atomic_failure" 81 | path = "../tests/atomic_failure.rs" 82 | required-features = ["std", "run"] 83 | 84 | [[test]] 85 | name = "atomic_success" 86 | path = "../tests/atomic_success.rs" 87 | required-features = ["std", "run"] 88 | 89 | [[test]] 90 | harness = false 91 | name = "web_instant_failure_1" 92 | path = "../tests/instant_failure_1.rs" 93 | required-features = ["run"] 94 | 95 | [[test]] 96 | harness = false 97 | name = "web_instant_failure_2" 98 | path = "../tests/instant_failure_2.rs" 99 | required-features = ["run"] 100 | 101 | [[test]] 102 | harness = false 103 | name = "web_instant_success" 104 | path = "../tests/instant_success.rs" 105 | required-features = ["run"] 106 | 107 | [[test]] 108 | harness = false 109 | name = "web_serde" 110 | path = "../tests/serde.rs" 111 | required-features = ["serde", "run"] 112 | 113 | [[test]] 114 | harness = false 115 | name = "web_system_time_failure_1" 116 | path = "../tests/system_time_failure_1.rs" 117 | required-features = ["run"] 118 | 119 | [[test]] 120 | harness = false 121 | name = "web_system_time_failure_2" 122 | path = "../tests/system_time_failure_2.rs" 123 | required-features = ["run"] 124 | 125 | [[test]] 126 | harness = false 127 | name = "web_system_time_success" 128 | path = "../tests/system_time_success.rs" 129 | required-features = ["run"] 130 | 131 | [[test]] 132 | harness = false 133 | name = "web_traits" 134 | path = "../tests/traits.rs" 135 | required-features = ["run"] 136 | 137 | [[test]] 138 | name = "web" 139 | path = "../tests/web.rs" 140 | required-features = ["std", "run"] 141 | 142 | [lints] 143 | workspace = true 144 | -------------------------------------------------------------------------------- /tests-web/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A crate for running tests on Web without the default test harness. 2 | 3 | #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std)] 4 | #![cfg_attr(all(test, target_arch = "wasm32"), no_main)] 5 | 6 | #[cfg(all(target_arch = "wasm32", not(feature = "std")))] 7 | use wasm_bindgen_test as _; 8 | 9 | #[cfg(all(test, not(target_arch = "wasm32")))] 10 | fn main() {} 11 | 12 | #[cfg(all(target_arch = "wasm32", not(feature = "std")))] 13 | #[expect( 14 | unsafe_code, 15 | reason = "no way to implement `GlobalAlloc` without unsafe" 16 | )] 17 | mod allocator { 18 | //! Implementing [`GlobalAlloc`]. 19 | //! 20 | //! See . 21 | 22 | use core::alloc::{GlobalAlloc, Layout}; 23 | 24 | use dlmalloc::Dlmalloc; 25 | 26 | /// The allocator. 27 | static mut DLMALLOC: Dlmalloc = Dlmalloc::new(); 28 | /// Global allocator. 29 | #[global_allocator] 30 | static ALLOC: System = System; 31 | 32 | /// Implementing [`GlobalAlloc`]. 33 | struct System; 34 | 35 | // SAFETY: we mostly rely on `dlmalloc` for safety. 36 | unsafe impl GlobalAlloc for System { 37 | #[inline] 38 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 { 39 | let _lock = lock::lock(); 40 | // SAFETY: `DLMALLOC` access is guaranteed to be safe because the lock gives us 41 | // unique and non-reentrant access. Calling `malloc()` is safe because 42 | // preconditions on this function match the trait method preconditions. 43 | unsafe { (*core::ptr::addr_of_mut!(DLMALLOC)).malloc(layout.size(), layout.align()) } 44 | } 45 | 46 | #[inline] 47 | unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { 48 | let _lock = lock::lock(); 49 | // SAFETY: `DLMALLOC` access is guaranteed to be safe because the lock gives us 50 | // unique and non-reentrant access. Calling `calloc()` is safe because 51 | // preconditions on this function match the trait method preconditions. 52 | unsafe { (*core::ptr::addr_of_mut!(DLMALLOC)).calloc(layout.size(), layout.align()) } 53 | } 54 | 55 | #[inline] 56 | unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { 57 | let _lock = lock::lock(); 58 | // SAFETY: `DLMALLOC` access is guaranteed to be safe because the lock gives us 59 | // unique and non-reentrant access. Calling `free()` is safe because 60 | // preconditions on this function match the trait method preconditions. 61 | unsafe { (*core::ptr::addr_of_mut!(DLMALLOC)).free(ptr, layout.size(), layout.align()) } 62 | } 63 | 64 | #[inline] 65 | unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { 66 | let _lock = lock::lock(); 67 | // SAFETY: `DLMALLOC` access is guaranteed to be safe because the lock gives us 68 | // unique and non-reentrant access. Calling `realloc()` is safe because 69 | // preconditions on this function match the trait method preconditions. 70 | unsafe { 71 | (*core::ptr::addr_of_mut!(DLMALLOC)).realloc( 72 | ptr, 73 | layout.size(), 74 | layout.align(), 75 | new_size, 76 | ) 77 | } 78 | } 79 | } 80 | 81 | /// The lock guard. 82 | // CHANGED: Add `#[must_used]` for safety. 83 | #[must_use = "if unused it will immediately unlock"] 84 | struct DropLock; 85 | 86 | /// Lock implementation. 87 | #[cfg(target_feature = "atomics")] 88 | mod lock { 89 | use core::sync::atomic::{AtomicBool, Ordering}; 90 | 91 | use super::DropLock; 92 | 93 | /// The lock flag. 94 | // CHANGED: using an `AtomicBool` instead of an `AtomicU32`. 95 | static LOCKED: AtomicBool = AtomicBool::new(false); 96 | 97 | /// Locks the thread until available. 98 | pub(super) fn lock() -> DropLock { 99 | loop { 100 | if !LOCKED.swap(true, Ordering::Acquire) { 101 | return DropLock; 102 | } 103 | } 104 | } 105 | 106 | impl Drop for DropLock { 107 | fn drop(&mut self) { 108 | LOCKED.swap(false, Ordering::Release); 109 | } 110 | } 111 | } 112 | 113 | /// Empty lock implementation when threads are not available. 114 | #[cfg(not(target_feature = "atomics"))] 115 | mod lock { 116 | use super::DropLock; 117 | 118 | /// Locks the thread until available. 119 | #[expect( 120 | clippy::missing_const_for_fn, 121 | reason = "compatibility with non-atomic lock" 122 | )] 123 | pub(super) fn lock() -> DropLock { 124 | DropLock 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/system_time_success.rs: -------------------------------------------------------------------------------- 1 | //! [`SystemTime`] tests. 2 | 3 | #![cfg(test)] 4 | #![cfg_attr(target_arch = "wasm32", no_main)] 5 | #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std)] 6 | 7 | extern crate alloc; 8 | 9 | mod util; 10 | 11 | use alloc::string::ToString; 12 | 13 | use wasm_bindgen_test::wasm_bindgen_test; 14 | use web_time::{Duration, SystemTime}; 15 | 16 | use self::util::{sleep, DIFF, MAX_DIFF, WAIT}; 17 | 18 | /// [`SystemTime::UNIX_EPOCH`]. 19 | #[allow( 20 | clippy::allow_attributes, 21 | clippy::eq_op, 22 | reason = "thats what we are testing" 23 | )] 24 | #[wasm_bindgen_test(unsupported = test)] 25 | fn unix_epoch() { 26 | let time = SystemTime::UNIX_EPOCH.elapsed().unwrap(); 27 | assert_eq!(time - time, Duration::ZERO); 28 | } 29 | 30 | /// [`SystemTime::duration_since()`] success. 31 | #[wasm_bindgen_test(unsupported = pollster::test)] 32 | async fn duration_since_success() { 33 | let time = SystemTime::now(); 34 | sleep(WAIT).await; 35 | let duration = SystemTime::now().duration_since(time).unwrap(); 36 | assert!(duration >= DIFF); 37 | assert!(duration <= MAX_DIFF); 38 | } 39 | 40 | /// [`SystemTime::duration_since()`] failure. 41 | #[wasm_bindgen_test(unsupported = pollster::test)] 42 | async fn duration_since_failure() { 43 | let time = SystemTime::now(); 44 | sleep(WAIT).await; 45 | let error = time.duration_since(SystemTime::now()).unwrap_err(); 46 | let duration = error.duration(); 47 | assert!(duration >= DIFF); 48 | assert!(duration <= MAX_DIFF); 49 | } 50 | 51 | /// [`SystemTime::elapsed()`] success. 52 | #[wasm_bindgen_test(unsupported = pollster::test)] 53 | async fn elapsed_success() { 54 | let time = SystemTime::now(); 55 | sleep(WAIT).await; 56 | let duration = time.elapsed().unwrap(); 57 | assert!(duration >= DIFF); 58 | assert!(duration <= MAX_DIFF); 59 | } 60 | 61 | /// [`SystemTime::elapsed()`] failure. 62 | #[wasm_bindgen_test(unsupported = test)] 63 | fn elapsed_failure() { 64 | let time = SystemTime::now() + WAIT; 65 | let error = time.elapsed().unwrap_err(); 66 | assert!(error.duration() <= WAIT); 67 | } 68 | 69 | /// [`SystemTime::checked_add()`] success. 70 | #[wasm_bindgen_test(unsupported = pollster::test)] 71 | async fn checked_add_success() { 72 | let time = SystemTime::now(); 73 | sleep(WAIT).await; 74 | let now = SystemTime::now(); 75 | assert!(time.checked_add(DIFF).unwrap() <= now); 76 | assert!(time.checked_add(MAX_DIFF).unwrap() >= now); 77 | } 78 | 79 | /// [`SystemTime::checked_add()`] failure. 80 | #[wasm_bindgen_test(unsupported = pollster::test)] 81 | async fn checked_add_failure() { 82 | sleep(WAIT).await; 83 | assert_eq!(SystemTime::now().checked_add(Duration::MAX), None); 84 | } 85 | 86 | /// [`SystemTime::checked_sub()`] success. 87 | #[wasm_bindgen_test(unsupported = pollster::test)] 88 | async fn checked_sub_success() { 89 | let time = SystemTime::now(); 90 | sleep(WAIT).await; 91 | let now = SystemTime::now(); 92 | assert!(now.checked_sub(DIFF).unwrap() >= time); 93 | assert!(now.checked_sub(MAX_DIFF).unwrap_or(SystemTime::UNIX_EPOCH) <= time); 94 | } 95 | 96 | /// [`SystemTime::checked_sub()`] failure. 97 | #[wasm_bindgen_test(unsupported = test)] 98 | fn checked_sub_failure() { 99 | assert_eq!(SystemTime::now().checked_sub(Duration::MAX), None); 100 | } 101 | 102 | /// [`SystemTime::add()`] success. 103 | #[wasm_bindgen_test(unsupported = pollster::test)] 104 | async fn add_success() { 105 | let time = SystemTime::now(); 106 | sleep(WAIT).await; 107 | assert!(time + DIFF <= SystemTime::now()); 108 | assert!(time + MAX_DIFF >= SystemTime::now()); 109 | } 110 | 111 | /// [`SystemTime::add_assign()`] success. 112 | #[wasm_bindgen_test(unsupported = pollster::test)] 113 | async fn add_assign_success() { 114 | let mut time_1 = SystemTime::now(); 115 | let mut time_2 = time_1; 116 | sleep(WAIT).await; 117 | let now = SystemTime::now(); 118 | time_1 += DIFF; 119 | assert!(time_1 <= now); 120 | time_2 += MAX_DIFF; 121 | assert!(time_2 >= now); 122 | } 123 | 124 | /// [`SystemTime::sub()`] success. 125 | #[wasm_bindgen_test(unsupported = pollster::test)] 126 | async fn sub_success() { 127 | let time = SystemTime::now(); 128 | sleep(WAIT).await; 129 | let now = SystemTime::now(); 130 | assert!(now - DIFF >= time); 131 | assert!(now.duration_since(time).unwrap() <= MAX_DIFF); 132 | } 133 | 134 | /// [`SystemTime::sub_assign()`] success. 135 | #[wasm_bindgen_test(unsupported = pollster::test)] 136 | async fn sub_assign_success() { 137 | let earlier = SystemTime::now(); 138 | sleep(WAIT).await; 139 | let mut later = SystemTime::now(); 140 | later -= DIFF; 141 | assert!(later >= earlier); 142 | assert!(later.duration_since(earlier).unwrap() <= MAX_DIFF); 143 | } 144 | 145 | /// [`SystemTime::elapsed()`] failure. 146 | #[wasm_bindgen_test(unsupported = test)] 147 | fn error() { 148 | let time = SystemTime::now() + DIFF; 149 | let error = time.elapsed().unwrap_err(); 150 | assert_eq!( 151 | error.to_string(), 152 | "second time provided was later than self" 153 | ); 154 | } 155 | -------------------------------------------------------------------------------- /src/time/serde.rs: -------------------------------------------------------------------------------- 1 | //! Serde serialization and de-serialization for `SystemTime`. It aims to be 2 | //! compatible with Serde's implementation for [`std::time::SystemTime`]. 3 | //! 4 | //! This implementation was copied from Serde's 5 | //! [`Deserialize`](https://github.com/serde-rs/serde/blob/v1.0.215/serde/src/de/impls.rs#L2282-L2426), 6 | //! and 7 | //! [`Serialize`](https://github.com/serde-rs/serde/blob/v1.0.215/serde/src/ser/impls.rs#L753-L768) 8 | //! implementation. 9 | 10 | #![allow(warnings)] 11 | 12 | // CHANGED: Replaced occurrences of `tri!` macro with `?`. 13 | 14 | extern crate alloc; 15 | 16 | use alloc::string::String; 17 | use core::fmt; 18 | use core::time::Duration; 19 | 20 | use serde::de::{Error, MapAccess, SeqAccess, Visitor}; 21 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 22 | 23 | use super::SystemTime; 24 | 25 | #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] 26 | impl<'de> Deserialize<'de> for SystemTime { 27 | fn deserialize(deserializer: D) -> Result 28 | where 29 | D: Deserializer<'de>, 30 | { 31 | // Reuse duration 32 | enum Field { 33 | Secs, 34 | Nanos, 35 | } 36 | 37 | impl<'de> Deserialize<'de> for Field { 38 | fn deserialize(deserializer: D) -> Result 39 | where 40 | D: Deserializer<'de>, 41 | { 42 | struct FieldVisitor; 43 | 44 | impl<'de> Visitor<'de> for FieldVisitor { 45 | type Value = Field; 46 | 47 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | formatter.write_str("`secs_since_epoch` or `nanos_since_epoch`") 49 | } 50 | 51 | fn visit_str(self, value: &str) -> Result 52 | where 53 | E: Error, 54 | { 55 | match value { 56 | "secs_since_epoch" => Ok(Field::Secs), 57 | "nanos_since_epoch" => Ok(Field::Nanos), 58 | _ => Err(Error::unknown_field(value, FIELDS)), 59 | } 60 | } 61 | 62 | fn visit_bytes(self, value: &[u8]) -> Result 63 | where 64 | E: Error, 65 | { 66 | match value { 67 | b"secs_since_epoch" => Ok(Field::Secs), 68 | b"nanos_since_epoch" => Ok(Field::Nanos), 69 | _ => { 70 | let value = String::from_utf8_lossy(value); 71 | Err(Error::unknown_field(&value, FIELDS)) 72 | } 73 | } 74 | } 75 | } 76 | 77 | deserializer.deserialize_identifier(FieldVisitor) 78 | } 79 | } 80 | 81 | fn check_overflow(secs: u64, nanos: u32) -> Result<(), E> 82 | where 83 | E: Error, 84 | { 85 | static NANOS_PER_SEC: u32 = 1_000_000_000; 86 | match secs.checked_add((nanos / NANOS_PER_SEC) as u64) { 87 | Some(_) => Ok(()), 88 | None => Err(E::custom("overflow deserializing SystemTime epoch offset")), 89 | } 90 | } 91 | 92 | struct DurationVisitor; 93 | 94 | impl<'de> Visitor<'de> for DurationVisitor { 95 | type Value = Duration; 96 | 97 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 98 | formatter.write_str("struct SystemTime") 99 | } 100 | 101 | fn visit_seq(self, mut seq: A) -> Result 102 | where 103 | A: SeqAccess<'de>, 104 | { 105 | let secs: u64 = match seq.next_element()? { 106 | Some(value) => value, 107 | None => { 108 | return Err(Error::invalid_length(0, &self)); 109 | } 110 | }; 111 | let nanos: u32 = match seq.next_element()? { 112 | Some(value) => value, 113 | None => { 114 | return Err(Error::invalid_length(1, &self)); 115 | } 116 | }; 117 | check_overflow(secs, nanos)?; 118 | Ok(Duration::new(secs, nanos)) 119 | } 120 | 121 | fn visit_map(self, mut map: A) -> Result 122 | where 123 | A: MapAccess<'de>, 124 | { 125 | let mut secs: Option = None; 126 | let mut nanos: Option = None; 127 | while let Some(key) = map.next_key()? { 128 | match key { 129 | Field::Secs => { 130 | if secs.is_some() { 131 | return Err(::duplicate_field( 132 | "secs_since_epoch", 133 | )); 134 | } 135 | secs = Some(map.next_value()?); 136 | } 137 | Field::Nanos => { 138 | if nanos.is_some() { 139 | return Err(::duplicate_field( 140 | "nanos_since_epoch", 141 | )); 142 | } 143 | nanos = Some(map.next_value()?); 144 | } 145 | } 146 | } 147 | let secs = match secs { 148 | Some(secs) => secs, 149 | None => return Err(::missing_field("secs_since_epoch")), 150 | }; 151 | let nanos = match nanos { 152 | Some(nanos) => nanos, 153 | None => return Err(::missing_field("nanos_since_epoch")), 154 | }; 155 | check_overflow(secs, nanos)?; 156 | Ok(Duration::new(secs, nanos)) 157 | } 158 | } 159 | 160 | const FIELDS: &[&str] = &["secs_since_epoch", "nanos_since_epoch"]; 161 | let duration = deserializer.deserialize_struct("SystemTime", FIELDS, DurationVisitor)?; 162 | // CHANGED: Insert `Duration` directly. 163 | let ret = Ok(Self(duration)); 164 | ret 165 | } 166 | } 167 | 168 | #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] 169 | impl Serialize for SystemTime { 170 | fn serialize(&self, serializer: S) -> Result 171 | where 172 | S: Serializer, 173 | { 174 | use serde::ser::SerializeStruct; 175 | // CHANGED: Take `Duration` directly. 176 | let duration_since_epoch = self.0; 177 | let mut state = serializer.serialize_struct("SystemTime", 2)?; 178 | state.serialize_field("secs_since_epoch", &duration_since_epoch.as_secs())?; 179 | state.serialize_field("nanos_since_epoch", &duration_since_epoch.subsec_nanos())?; 180 | state.end() 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/time/system_time.rs: -------------------------------------------------------------------------------- 1 | //! Re-implementation of [`std::time::SystemTime`]. 2 | //! 3 | //! See . 4 | #![cfg_attr( 5 | not(feature = "std"), 6 | doc = "", 7 | doc = "[`std::time::SystemTime`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html" 8 | )] 9 | 10 | #[cfg(all(all(doc, docsrs), not(feature = "std")))] 11 | use core::error::Error; 12 | use core::fmt::{self, Display, Formatter}; 13 | use core::ops::{Add, AddAssign, Sub, SubAssign}; 14 | use core::time::Duration; 15 | #[cfg(feature = "std")] 16 | use std::error::Error; 17 | 18 | use super::js::Date; 19 | 20 | /// See [`std::time::SystemTime`]. 21 | #[cfg_attr( 22 | not(feature = "std"), 23 | doc = "", 24 | doc = "[`std::time::SystemTime`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html" 25 | )] 26 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 27 | pub struct SystemTime(pub(crate) Duration); 28 | 29 | impl SystemTime { 30 | /// See [`std::time::SystemTime::UNIX_EPOCH`]. 31 | #[cfg_attr( 32 | not(feature = "std"), 33 | doc = "", 34 | doc = "[`std::time::SystemTime::UNIX_EPOCH`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html#associatedconstant.UNIX_EPOCH" 35 | )] 36 | pub const UNIX_EPOCH: Self = Self(Duration::ZERO); 37 | 38 | /// See [`std::time::SystemTime::now()`]. 39 | #[cfg_attr( 40 | not(feature = "std"), 41 | doc = "", 42 | doc = "[`std::time::SystemTime::now()`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html#method.now" 43 | )] 44 | #[must_use] 45 | #[allow(clippy::missing_panics_doc)] 46 | pub fn now() -> Self { 47 | #[allow(clippy::as_conversions, clippy::cast_possible_truncation)] 48 | let ms = Date::now() as i64; 49 | let ms = ms.try_into().expect("found negative timestamp"); 50 | 51 | Self(Duration::from_millis(ms)) 52 | } 53 | 54 | /// See [`std::time::SystemTime::duration_since()`]. 55 | #[cfg_attr( 56 | not(feature = "std"), 57 | doc = "", 58 | doc = "[`std::time::SystemTime::duration_since()`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html#method.duration_since" 59 | )] 60 | #[allow(clippy::missing_errors_doc, clippy::trivially_copy_pass_by_ref)] 61 | pub fn duration_since(&self, earlier: Self) -> Result { 62 | // See . 63 | self.0 64 | .checked_sub(earlier.0) 65 | .ok_or_else(|| SystemTimeError(earlier.0 - self.0)) 66 | } 67 | 68 | /// See [`std::time::SystemTime::elapsed()`]. 69 | #[cfg_attr( 70 | not(feature = "std"), 71 | doc = "", 72 | doc = "[`std::time::SystemTime::elapsed()`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html#method.elapsed" 73 | )] 74 | #[allow(clippy::missing_errors_doc, clippy::trivially_copy_pass_by_ref)] 75 | pub fn elapsed(&self) -> Result { 76 | Self::now().duration_since(*self) 77 | } 78 | 79 | /// See [`std::time::SystemTime::checked_add()`]. 80 | #[cfg_attr( 81 | not(feature = "std"), 82 | doc = "", 83 | doc = "[`std::time::SystemTime::checked_add()`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html#method.checked_add" 84 | )] 85 | #[allow(clippy::trivially_copy_pass_by_ref)] 86 | pub fn checked_add(&self, duration: Duration) -> Option { 87 | self.0.checked_add(duration).map(SystemTime) 88 | } 89 | 90 | /// See [`std::time::SystemTime::checked_sub()`]. 91 | #[cfg_attr( 92 | not(feature = "std"), 93 | doc = "", 94 | doc = "[`std::time::SystemTime::checked_sub()`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html#method.checked_sub" 95 | )] 96 | #[allow(clippy::trivially_copy_pass_by_ref)] 97 | pub fn checked_sub(&self, duration: Duration) -> Option { 98 | self.0.checked_sub(duration).map(SystemTime) 99 | } 100 | } 101 | 102 | impl Add for SystemTime { 103 | type Output = Self; 104 | 105 | /// # Panics 106 | /// 107 | /// This function may panic if the resulting point in time cannot be 108 | /// represented by the underlying data structure. See 109 | /// [`SystemTime::checked_add`] for a version without panic. 110 | fn add(self, rhs: Duration) -> Self { 111 | self.checked_add(rhs) 112 | .expect("overflow when adding duration to instant") 113 | } 114 | } 115 | 116 | impl AddAssign for SystemTime { 117 | fn add_assign(&mut self, rhs: Duration) { 118 | *self = *self + rhs; 119 | } 120 | } 121 | 122 | impl Sub for SystemTime { 123 | type Output = Self; 124 | 125 | fn sub(self, rhs: Duration) -> Self { 126 | self.checked_sub(rhs) 127 | .expect("overflow when subtracting duration from instant") 128 | } 129 | } 130 | 131 | impl SubAssign for SystemTime { 132 | fn sub_assign(&mut self, rhs: Duration) { 133 | *self = *self - rhs; 134 | } 135 | } 136 | 137 | /// See [`std::time::SystemTimeError`]. 138 | #[cfg_attr( 139 | not(feature = "std"), 140 | doc = "", 141 | doc = "[`std::time::SystemTimeError`]: https://doc.rust-lang.org/std/time/struct.SystemTimeError.html" 142 | )] 143 | #[derive(Clone, Debug)] 144 | #[allow(missing_copy_implementations)] 145 | pub struct SystemTimeError(Duration); 146 | 147 | impl SystemTimeError { 148 | /// See [`std::time::SystemTimeError::duration()`]. 149 | #[cfg_attr( 150 | not(feature = "std"), 151 | doc = "", 152 | doc = "[`std::time::SystemTimeError::duration()`]: https://doc.rust-lang.org/std/time/struct.SystemTimeError.html#method.duration" 153 | )] 154 | #[must_use] 155 | #[allow(clippy::missing_const_for_fn)] 156 | pub fn duration(&self) -> Duration { 157 | self.0 158 | } 159 | } 160 | 161 | impl Display for SystemTimeError { 162 | fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 163 | write!(formatter, "second time provided was later than self") 164 | } 165 | } 166 | 167 | #[cfg(any(feature = "std", all(doc, docsrs)))] 168 | #[cfg_attr(all(doc, docsrs), doc(cfg(feature = "std")))] 169 | impl Error for SystemTimeError {} 170 | -------------------------------------------------------------------------------- /tests/instant_success.rs: -------------------------------------------------------------------------------- 1 | //! [`Instant`] tests. 2 | 3 | #![cfg(test)] 4 | #![cfg_attr(target_arch = "wasm32", no_main)] 5 | #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std)] 6 | 7 | mod util; 8 | 9 | use wasm_bindgen_test::wasm_bindgen_test; 10 | use web_time::{Duration, Instant}; 11 | 12 | use self::util::{sleep, DIFF, MAX_DIFF, WAIT}; 13 | 14 | /// [`Instant::duration_since()`] success. 15 | #[wasm_bindgen_test(unsupported = pollster::test)] 16 | async fn duration_success() { 17 | let instant = Instant::now(); 18 | sleep(WAIT).await; 19 | let duration = Instant::now().duration_since(instant); 20 | assert!(duration >= DIFF, "{duration:?}"); 21 | assert!(duration <= MAX_DIFF); 22 | } 23 | 24 | /// [`Instant::duration_since()`] failure. 25 | #[wasm_bindgen_test(unsupported = pollster::test)] 26 | async fn duration_failure() { 27 | let instant = Instant::now(); 28 | sleep(WAIT).await; 29 | assert_eq!(instant.duration_since(Instant::now()), Duration::ZERO); 30 | } 31 | 32 | /// [`Instant::checked_duration_since()`] success. 33 | #[wasm_bindgen_test(unsupported = pollster::test)] 34 | async fn checked_duration_success() { 35 | let instant = Instant::now(); 36 | sleep(WAIT).await; 37 | let duration = Instant::now().checked_duration_since(instant); 38 | assert!(duration >= Some(DIFF), "{duration:?}"); 39 | assert!(duration <= Some(MAX_DIFF)); 40 | } 41 | 42 | /// [`Instant::checked_duration_since()`] failure. 43 | #[wasm_bindgen_test(unsupported = pollster::test)] 44 | async fn checked_duration_failure() { 45 | let instant = Instant::now(); 46 | sleep(WAIT).await; 47 | assert_eq!(instant.checked_duration_since(Instant::now()), None); 48 | } 49 | 50 | /// [`Instant::saturating_duration_since()`] success. 51 | #[wasm_bindgen_test(unsupported = pollster::test)] 52 | async fn saturating_duration_success() { 53 | let instant = Instant::now(); 54 | sleep(WAIT).await; 55 | let duration = Instant::now().saturating_duration_since(instant); 56 | assert!(duration >= DIFF, "{duration:?}"); 57 | assert!(duration <= MAX_DIFF); 58 | } 59 | 60 | /// [`Instant::saturating_duration_fail()`] success. 61 | #[wasm_bindgen_test(unsupported = pollster::test)] 62 | async fn saturating_duration_failure() { 63 | let instant = Instant::now(); 64 | sleep(WAIT).await; 65 | assert_eq!( 66 | instant.saturating_duration_since(Instant::now()), 67 | Duration::ZERO 68 | ); 69 | } 70 | 71 | /// [`Instant::elapsed()`]. 72 | #[wasm_bindgen_test(unsupported = pollster::test)] 73 | async fn elapsed() { 74 | let instant = Instant::now(); 75 | sleep(WAIT).await; 76 | let duration = instant.elapsed(); 77 | assert!(duration >= DIFF, "{duration:?}"); 78 | assert!(duration <= MAX_DIFF); 79 | } 80 | 81 | /// [`Instant::checked_add()`] success. 82 | #[wasm_bindgen_test(unsupported = pollster::test)] 83 | async fn checked_add_success() { 84 | let instant = Instant::now(); 85 | sleep(WAIT).await; 86 | let now = Instant::now(); 87 | assert!( 88 | instant.checked_add(DIFF).unwrap() <= now, 89 | "{:?}", 90 | now - instant 91 | ); 92 | assert!(instant.checked_add(MAX_DIFF).unwrap() >= now); 93 | } 94 | 95 | /// [`Instant::checked_add()`] failure. 96 | #[wasm_bindgen_test(unsupported = pollster::test)] 97 | async fn checked_add_failure() { 98 | sleep(WAIT).await; 99 | assert_eq!(Instant::now().checked_add(Duration::MAX), None); 100 | } 101 | 102 | /// [`Instant::checked_sub()`] success. 103 | #[wasm_bindgen_test(unsupported = pollster::test)] 104 | async fn checked_sub_success() { 105 | let instant = Instant::now(); 106 | sleep(WAIT).await; 107 | let now = Instant::now(); 108 | assert!( 109 | now.checked_sub(DIFF).unwrap() >= instant, 110 | "{:?}", 111 | now - instant 112 | ); 113 | assert!(now.duration_since(instant) <= MAX_DIFF); 114 | } 115 | 116 | /// [`Instant::checked_sub()`] failure. 117 | #[wasm_bindgen_test(unsupported = test)] 118 | fn checked_sub_failure() { 119 | assert_eq!(Instant::now().checked_sub(Duration::MAX), None); 120 | } 121 | 122 | /// [`Instant::add()`] success. 123 | #[wasm_bindgen_test(unsupported = pollster::test)] 124 | async fn add_success() { 125 | let instant = Instant::now(); 126 | sleep(WAIT).await; 127 | let now = Instant::now(); 128 | assert!(instant + DIFF <= now, "{:?}", now - instant); 129 | assert!(instant + MAX_DIFF >= now); 130 | } 131 | 132 | /// [`Instant::add_assign()`] success. 133 | #[wasm_bindgen_test(unsupported = pollster::test)] 134 | async fn add_assign_success() { 135 | let mut instant = Instant::now(); 136 | sleep(WAIT).await; 137 | let now = Instant::now(); 138 | instant += DIFF; 139 | assert!(instant <= now, "{:?}", now - instant); 140 | instant += MAX_DIFF; 141 | assert!(instant >= now); 142 | } 143 | 144 | /// [`Instant::sub()`] success. 145 | #[wasm_bindgen_test(unsupported = pollster::test)] 146 | #[allow( 147 | clippy::allow_attributes, 148 | clippy::unchecked_duration_subtraction, 149 | reason = "this is what we are testing" 150 | )] 151 | async fn sub_success() { 152 | let instant = Instant::now(); 153 | sleep(WAIT).await; 154 | let now = Instant::now(); 155 | assert!(now - DIFF >= instant, "{:?}", now - instant); 156 | assert!(now.duration_since(instant) <= MAX_DIFF); 157 | } 158 | 159 | /// [`Instant::sub_assign()`] success. 160 | #[wasm_bindgen_test(unsupported = pollster::test)] 161 | async fn sub_assign_success() { 162 | let earlier = Instant::now(); 163 | sleep(WAIT).await; 164 | let mut later = Instant::now(); 165 | later -= DIFF; 166 | assert!(later >= earlier); 167 | assert!(later.duration_since(earlier) <= MAX_DIFF); 168 | } 169 | 170 | /// [`Self`] comparisons. 171 | #[wasm_bindgen_test(unsupported = pollster::test)] 172 | async fn comparison() { 173 | let earlier = Instant::now(); 174 | 175 | let later = Instant::now(); 176 | assert!(earlier <= later, "{:?}", earlier - later); 177 | 178 | sleep(WAIT).await; 179 | 180 | let later = Instant::now(); 181 | assert!((later - earlier) >= DIFF, "{:?}", later - earlier); 182 | 183 | let later = Instant::now(); 184 | assert!(earlier <= later, "{:?}", earlier - later); 185 | } 186 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["tests-native", "tests-web"] 3 | resolver = "2" 4 | 5 | [package] 6 | autobenches = false 7 | autotests = false 8 | categories = ["api-bindings", "date-and-time", "wasm"] 9 | description = "Drop-in replacement for std::time for Wasm in browsers" 10 | edition = "2021" 11 | include = ["CHANGELOG.md", "LICENSE-*", "src/**/*"] 12 | keywords = ["instant", "wasm", "web", "systemtime", "time"] 13 | license = "MIT OR Apache-2.0" 14 | name = "web-time" 15 | repository = "https://github.com/daxpedda/web-time" 16 | rust-version = "1.60" 17 | version = "1.1.0" 18 | 19 | [features] 20 | default = ["std", "msrv"] 21 | msrv = ["dep:rustversion"] 22 | serde = ["dep:serde"] 23 | std = ["wasm-bindgen-test/std", "getrandom/std", "rand/std", "tests-native/std", "tests-web/std"] 24 | 25 | [target.'cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))'.dependencies] 26 | serde = { version = "1.0.0", optional = true, default-features = false } 27 | wasm-bindgen = { version = "0.2.98", default-features = false } 28 | 29 | [build-dependencies] 30 | rustversion = { version = "1.0.0", optional = true } 31 | 32 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 33 | tests-native = { path = "tests-native", default-features = false, features = ["run"] } 34 | 35 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 36 | getrandom = { version = "0.2", features = ["js"] } 37 | rand = { version = "0.8", default-features = false, features = ["getrandom", "std_rng"] } 38 | tests-web = { path = "tests-web", default-features = false, features = ["run"] } 39 | wasm-bindgen-test = { version = "0.3", default-features = false, features = [ 40 | "msrv", 41 | "unstable-test-coverage", 42 | ] } 43 | 44 | [patch.crates-io] 45 | getrandom = { git = "https://github.com/daxpedda/getrandom", branch = "web-time" } 46 | js-sys = { git = "https://github.com/daxpedda/wasm-bindgen", branch = "web-time" } 47 | minicov = { git = "https://github.com/daxpedda/minicov", branch = "web-time" } 48 | serde_test = { git = "https://github.com/daxpedda/test", branch = "no_std" } 49 | wasm-bindgen = { git = "https://github.com/daxpedda/wasm-bindgen", branch = "web-time" } 50 | wasm-bindgen-futures = { git = "https://github.com/daxpedda/wasm-bindgen", branch = "web-time" } 51 | wasm-bindgen-test = { git = "https://github.com/daxpedda/wasm-bindgen", branch = "web-time" } 52 | web-sys = { git = "https://github.com/daxpedda/wasm-bindgen", branch = "web-time" } 53 | 54 | [profile.test] 55 | opt-level = 1 56 | 57 | [profile.bench] 58 | codegen-units = 1 59 | lto = true 60 | 61 | [lib] 62 | bench = false 63 | harness = false 64 | 65 | [package.metadata.docs.rs] 66 | all-features = true 67 | rustdoc-args = ["--cfg=docsrs"] 68 | targets = ["wasm32-unknown-unknown", "wasm32v1-none"] 69 | 70 | [lints] 71 | workspace = true 72 | 73 | [workspace.lints.rust] 74 | # Rust groups. 75 | future_incompatible = { level = "warn", priority = -1 } 76 | rust_2018_compatibility = { level = "warn", priority = -1 } 77 | rust_2018_idioms = { level = "warn", priority = -1 } 78 | rust_2021_compatibility = { level = "warn", priority = -1 } 79 | unused = { level = "warn", priority = -1 } 80 | # Rust lints. 81 | deprecated_in_future = "warn" 82 | ffi_unwind_calls = "warn" 83 | macro_use_extern_crate = "warn" 84 | meta_variable_misuse = "warn" 85 | missing_abi = "warn" 86 | missing_copy_implementations = "warn" 87 | missing_debug_implementations = "warn" 88 | missing_docs = "warn" 89 | non_ascii_idents = "warn" 90 | redundant_lifetimes = "warn" 91 | single_use_lifetimes = "warn" 92 | trivial_casts = "warn" 93 | trivial_numeric_casts = "warn" 94 | unexpected_cfgs = { level = "warn", check-cfg = [ 95 | 'cfg(v1_77)', 96 | 'cfg(nightly)', 97 | 'cfg(web_time_test_coverage)', 98 | ] } 99 | unnameable_types = "warn" 100 | unreachable_pub = "warn" 101 | unsafe_code = "deny" 102 | unsafe_op_in_unsafe_fn = "warn" 103 | unused_import_braces = "warn" 104 | unused_lifetimes = "warn" 105 | unused_qualifications = "warn" 106 | 107 | [workspace.lints.clippy] 108 | # Clippy groups. 109 | cargo = { level = "warn", priority = -1 } 110 | nursery = { level = "warn", priority = -1 } 111 | pedantic = { level = "warn", priority = -1 } 112 | # Clippy restriction lints. 113 | absolute_paths = "warn" 114 | allow_attributes = "warn" 115 | allow_attributes_without_reason = "warn" 116 | as_conversions = "warn" 117 | assertions_on_result_states = "warn" 118 | cfg_not_test = "warn" 119 | clone_on_ref_ptr = "warn" 120 | create_dir = "warn" 121 | dbg_macro = "warn" 122 | decimal_literal_representation = "warn" 123 | default_union_representation = "warn" 124 | empty_drop = "warn" 125 | empty_enum_variants_with_brackets = "warn" 126 | empty_structs_with_brackets = "warn" 127 | error_impl_error = "warn" 128 | exit = "warn" 129 | filetype_is_file = "warn" 130 | float_cmp_const = "warn" 131 | fn_to_numeric_cast_any = "warn" 132 | format_push_string = "warn" 133 | get_unwrap = "warn" 134 | if_then_some_else_none = "warn" 135 | impl_trait_in_params = "warn" 136 | indexing_slicing = "warn" 137 | infinite_loop = "warn" 138 | large_include_file = "warn" 139 | lossy_float_literal = "warn" 140 | mem_forget = "warn" 141 | min_ident_chars = "warn" 142 | missing_assert_message = "warn" 143 | missing_asserts_for_indexing = "warn" 144 | missing_docs_in_private_items = "warn" 145 | mixed_read_write_in_expression = "warn" 146 | mutex_atomic = "warn" 147 | non_ascii_literal = "warn" 148 | partial_pub_fields = "warn" 149 | print_stderr = "warn" 150 | print_stdout = "warn" 151 | pub_without_shorthand = "warn" 152 | rc_buffer = "warn" 153 | rc_mutex = "warn" 154 | redundant_type_annotations = "warn" 155 | ref_patterns = "warn" 156 | renamed_function_params = "warn" 157 | rest_pat_in_fully_bound_structs = "warn" 158 | same_name_method = "warn" 159 | self_named_module_files = "warn" 160 | semicolon_outside_block = "warn" 161 | single_char_lifetime_names = "warn" 162 | str_to_string = "warn" 163 | string_add = "warn" 164 | string_lit_chars_any = "warn" 165 | string_slice = "warn" 166 | string_to_string = "warn" 167 | suspicious_xor_used_as_pow = "warn" 168 | todo = "warn" 169 | try_err = "warn" 170 | undocumented_unsafe_blocks = "warn" 171 | unimplemented = "warn" 172 | unnecessary_safety_doc = "warn" 173 | unnecessary_self_imports = "warn" 174 | unneeded_field_pattern = "warn" 175 | unseparated_literal_suffix = "warn" 176 | unwrap_used = "warn" 177 | use_debug = "warn" 178 | verbose_file_reads = "warn" 179 | # Allowed Clippy lints. 180 | equatable_if_let = "allow" 181 | explicit_deref_methods = "allow" 182 | future_not_send = "allow" 183 | module_inception = "allow" 184 | module_name_repetitions = "allow" 185 | option_if_let_else = "allow" 186 | redundant_pub_crate = "allow" 187 | tabs_in_doc_comments = "allow" 188 | 189 | [workspace.lints.rustdoc] 190 | all = { level = "warn", priority = -1 } 191 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref_name }} 10 | cancel-in-progress: true 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | test: 17 | name: 18 | Test ${{ matrix.driver.description }} ${{ matrix.environment.description }} ${{ 19 | matrix.rust.description }} ${{ matrix.features.description }} 20 | 21 | runs-on: ${{ matrix.driver.os }} 22 | 23 | timeout-minutes: 10 24 | 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | target: 29 | - { target: wasm32-unknown-unknown, docargs: -Zdoctest-xcompile } 30 | rust: 31 | - { version: nightly } 32 | - { 33 | version: nightly, 34 | description: with Atomics, 35 | component: --component rust-src, 36 | cflags: -matomics -mbulk-memory, 37 | flags: "-Ctarget-feature=+atomics,+bulk-memory", 38 | build-std: true, 39 | atomics: true, 40 | } 41 | features: 42 | - { features: "", no_std: false } 43 | - { features: --no-default-features, no_std: true, description: (`no_std`) } 44 | - { 45 | features: --no-default-features --features msrv, 46 | no_std: true, 47 | description: "(`no_std`, `msrv`)", 48 | } 49 | driver: 50 | - { 51 | os: ubuntu-latest, 52 | description: Chrome, 53 | env: CHROMEDRIVER, 54 | binary: chromedriver, 55 | browser: true, 56 | } 57 | - { 58 | os: ubuntu-latest, 59 | description: Firefox, 60 | env: GECKODRIVER, 61 | binary: geckodriver, 62 | browser: true, 63 | firefox: true, 64 | } 65 | - { 66 | os: macos-latest, 67 | description: Safari, 68 | env: SAFARIDRIVER, 69 | binary: safaridriver, 70 | browser: true, 71 | macos: true, 72 | } 73 | - { os: ubuntu-24.04, description: Node.js, nodejs: true } 74 | environment: 75 | - { name: WASM_BINDGEN_USE_BROWSER, browser: true } 76 | - { 77 | description: Dedicated Worker, 78 | name: WASM_BINDGEN_USE_DEDICATED_WORKER, 79 | browser: true, 80 | } 81 | - { 82 | description: Shared Worker, 83 | name: WASM_BINDGEN_USE_SHARED_WORKER, 84 | browser: true, 85 | shared-worker: true, 86 | } 87 | - { 88 | description: Service Worker, 89 | name: WASM_BINDGEN_USE_SERVICE_WORKER, 90 | browser: true, 91 | service-worker: true, 92 | } 93 | - { nodejs: true, no-modules: true } 94 | - { description: ESM, name: WASM_BINDGEN_USE_NODE_EXPERIMENTAL, nodejs: true } 95 | include: 96 | - target: { target: x86_64-unknown-linux-gnu } 97 | rust: { version: stable } 98 | features: { features: "", no_std: false } 99 | driver: { os: ubuntu-latest, description: Native, native: true } 100 | exclude: 101 | - driver: { browser: true } 102 | environment: { browser: false } 103 | - driver: { nodejs: true } 104 | environment: { browser: true } 105 | # Firefox doesn't support `Atomics.waitAsync()` and the polyfill requires spawning workers. 106 | - driver: { firefox: true } 107 | rust: { atomics: true } 108 | environment: { shared-worker: true } 109 | # Firefox doesn't support module service workers. 110 | - driver: { firefox: true } 111 | environment: { service-worker: true } 112 | # Thread spawning is only supported for ESM 113 | - rust: { atomics: true } 114 | environment: { no-modules: true } 115 | 116 | steps: 117 | - name: Checkout 118 | uses: actions/checkout@v4 119 | - name: Install `wasm-bindgen-cli` 120 | uses: taiki-e/cache-cargo-install-action@v2 121 | with: 122 | tool: wasm-bindgen-cli 123 | git: https://github.com/daxpedda/wasm-bindgen 124 | rev: d4cb4a5d94090c18b469796250744612fd347dbd 125 | - name: Install Rust 126 | run: | 127 | rustup toolchain install ${{ matrix.rust.version }} --profile minimal ${{ matrix.rust.component }} --target ${{ matrix.target.target }} 128 | rustup default ${{ matrix.rust.version }} 129 | - name: Install Clang with `wasm32-unknown-unknown` support on MacOS 130 | if: matrix.driver.macos == true 131 | run: | 132 | brew install llvm 133 | echo "$(brew --prefix llvm)/bin" >> $GITHUB_PATH 134 | - name: Set `build-std` components 135 | if: matrix.rust.build-std == true && matrix.features.no_std == false 136 | run: echo "BUILD_STD_COMPONENTS=-Zbuild-std=panic_abort,std" >> $GITHUB_ENV 137 | - name: Set `build-std` `no_std` components 138 | if: matrix.rust.build-std == true && matrix.features.no_std == true 139 | run: echo "BUILD_STD_COMPONENTS=-Zbuild-std=core,alloc" >> $GITHUB_ENV 140 | - name: Start and set WebDriver 141 | if: matrix.driver.browser == true 142 | run: | 143 | iteration=5 144 | 145 | while true; do 146 | if (( iteration == 0 )); then 147 | echo "CI: Failed to start driver." 148 | exit 1 149 | fi 150 | 151 | (( iteration-- )) 152 | 153 | ${{ matrix.driver.binary }} --port=9000 2>stderr & 154 | process_pid=$! 155 | tail -f stderr >&2 & 156 | 157 | if [[ $(wc -l < stderr) -gt 0 ]]; then 158 | echo "CI: WebDriver failed" 159 | kill -SIGKILL $process_pid || true 160 | echo 161 | 162 | echo "CI: stderr:" 163 | sed 's/^/CI: /' stderr 164 | echo 165 | 166 | echo "CI: Re-trying to start the WebDriver." 167 | else 168 | echo "Successfully started WebDriver on port 9000." 169 | echo "${{ matrix.driver.env }}_REMOTE=http://127.0.0.1:9000" >> $GITHUB_ENV 170 | break 171 | fi 172 | done 173 | - name: Set environment 174 | if: matrix.environment.name != '' 175 | run: echo "${{ matrix.environment.name }}=1" >> $GITHUB_ENV 176 | - name: Test 177 | env: 178 | CFLAGS_wasm32_unknown_unknown: ${{ matrix.rust.cflags }} 179 | CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS: ${{ matrix.rust.flags }} 180 | RUSTDOCFLAGS: ${{ matrix.rust.flags }} 181 | run: 182 | cargo test --features serde ${{ matrix.features.features }} --target ${{ 183 | matrix.target.target }} $BUILD_STD_COMPONENTS --workspace ${{ matrix.target.docargs }} 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-time 2 | 3 | [![Crates.io Version](https://img.shields.io/crates/v/web-time.svg)](https://crates.io/crates/web-time) 4 | [![Live Build Status](https://img.shields.io/github/check-runs/daxpedda/web-time/main?label=CI)](https://github.com/daxpedda/web-time/actions?query=branch%3Amain) 5 | [![Docs.rs Documentation](https://img.shields.io/docsrs/web-time?label=docs.rs)](https://docs.rs/web-time/1.1.0) 6 | [![Main Documentation](https://img.shields.io/github/actions/workflow/status/daxpedda/web-time/coverage-documentation.yaml?branch=main&label=main%20docs)](https://daxpedda.github.io/web-time/doc/web_time) 7 | [![Test Coverage](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdaxpedda.github.io%2Fweb-time%2Fcoverage%2Fcoverage.json&query=%24.coverage&label=Test%20Coverage)](https://daxpedda.github.io/web-time/coverage) 8 | 9 | ## Description 10 | 11 | Complete drop-in replacement for [`std::time`] that works in browsers. 12 | 13 | Currently [`Instant::now()`] and [`SystemTime::now()`] will simply panic when using the 14 | `wasm32-unknown-unknown` target. This implementation uses [`Performance.now()`] for [`Instant`] and 15 | [`Date.now()`] for [`SystemTime`] to offer a drop-in replacement that works in browsers. 16 | 17 | At the same time the library will simply re-export [`std::time`] when not using the 18 | `wasm32-unknown-unknown` or `wasm32v1-none` target and will not pull in any dependencies. 19 | 20 | Additionally, if compiled with `target-feature = "atomics"` it will synchronize the timestamps to 21 | account for different context's, like web workers. See [`Performance.timeOrigin`] for more 22 | information. 23 | 24 | ## Target 25 | 26 | This library specifically targets browsers, that support [`Performance.now()`], with the 27 | `wasm32-unknown-unknown` or `wasm32v1-none` target. Emscripten is not supported. WASI doesn't 28 | require support as it has it's own native API to deal with [`std::time`]. 29 | 30 | Furthermore it depends on [`wasm-bindgen`], which is required. This library will continue to depend 31 | on it until a viable alternative presents itself, in which case multiple ecosystems could be 32 | supported. 33 | 34 | ## Note 35 | 36 | ### Ticking during sleep 37 | 38 | Currently a known bug is affecting browsers on operating system other then Windows. This bug 39 | prevents [`Instant`] from continuing to tick when the context is asleep. While this doesn't conflict 40 | with Rusts requirements of [`Instant`], by chance Rust's Std 41 | [has the same problem](https://github.com/rust-lang/rust/issues/79462). 42 | 43 | See 44 | [the MDN documentation on this](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#ticking_during_sleep) 45 | for more information. 46 | 47 | ### Context support 48 | 49 | The implementation of [`Instant::now()`] relies on the availability of the [`Performance` object], a 50 | lack thereof will cause a panic. This can happen if called from a [worklet]. 51 | 52 | ## Usage 53 | 54 | You can simply import the types you need: 55 | 56 | ```rust 57 | use web_time::{Instant, SystemTime}; 58 | 59 | let now = Instant::now(); 60 | let time = SystemTime::now(); 61 | ``` 62 | 63 | Using `-Ctarget-feature=+nontrapping-fptoint` will improve the performance of [`Instant::now()`] and 64 | [`SystemTime::now()`], but the vast majority of the time is still spent going through JS. 65 | 66 | ## Features 67 | 68 | ### `std` (enabled by default) 69 | 70 | Enables the corresponding crate feature in all dependencies and allows for some optimized 71 | instruction output. 72 | 73 | Without this crate feature compilation the standard library is not included. Has no effect on 74 | targets other then `wasm32-unknown-unknown` or `wasm32v1-none`. 75 | 76 | ### `msrv` (enabled by default) 77 | 78 | Allows `web-time` to make use of features only available in higher MSRVs. This offers compile-time 79 | detection and does not break compilation when enabled with the crates MSRV. 80 | 81 | - Rust v1.77 + `std`: Enables the use of the [`f64.nearest`] instruction. Which will significantly 82 | reduce the instruction count for [`Instant::now()`]. 83 | - Rust Nightly: Enables the use of the [`f64.trunc`] and [`f64.nearest`] instruction. Which will 84 | significantly reduce the instruction count for [`Instant::now()`]. 85 | 86 | ### `serde` 87 | 88 | Implements [`serde::Deserialize`] and [`serde::Serialize`] for [`SystemTime`]. 89 | 90 | ## Conditional Configurations 91 | 92 | ### `docsrs` 93 | 94 | This requires Rust nightly and enhances the documentation. It must only be used with `RUSTDOCFLAGS`, 95 | not with `RUSTFLAGS`. 96 | 97 | ## MSRV Policy 98 | 99 | The MSRV is v1.60. Changes to the MSRV will be accompanied by a minor version bump. 100 | 101 | ## Changelog 102 | 103 | See the [CHANGELOG] file for details. 104 | 105 | ## Contributing 106 | 107 | See the [CONTRIBUTING] file for details. 108 | 109 | ## Attribution 110 | 111 | Inspiration was taken from the [instant](https://github.com/sebcrozet/instant/tree/v0.1.12) project. 112 | 113 | Additional insight was taken from the [time](https://github.com/time-rs/time/tree/v0.3.20) project. 114 | 115 | ## License 116 | 117 | Licensed under either of 118 | 119 | - Apache License, Version 2.0 ([LICENSE-APACHE] or ) 120 | - MIT license ([LICENSE-MIT] or ) 121 | 122 | at your option. 123 | 124 | ### Copyright 125 | 126 | A majority of the code and documentation was taken from [`std::time`]. For license information see 127 | [#License](https://github.com/rust-lang/rust/tree/1.68.1#license). 128 | 129 | ### Contribution 130 | 131 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the 132 | work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 133 | additional terms or conditions. 134 | 135 | [CHANGELOG]: https://github.com/daxpedda/web-time/blob/v1.1.0/CHANGELOG.md 136 | [CONTRIBUTING]: https://github.com/daxpedda/web-time/blob/v1.1.0/CONTRIBUTING.md 137 | [LICENSE-MIT]: https://github.com/daxpedda/web-time/blob/v1.1.0/LICENSE-MIT 138 | [LICENSE-APACHE]: https://github.com/daxpedda/web-time/blob/v1.1.0/LICENSE-APACHE 139 | [worklet]: https://developer.mozilla.org/en-US/docs/Web/API/Worklet 140 | [`Date.now()`]: 141 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now 142 | [`Instant`]: https://doc.rust-lang.org/std/time/struct.Instant.html 143 | [`Instant::now()`]: https://doc.rust-lang.org/std/time/struct.Instant.html#method.now 144 | [`SystemTime`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html 145 | [`SystemTime::now()`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html#method.now 146 | [`std::time`]: https://doc.rust-lang.org/std/time/ 147 | [`performance.now()`]: https://developer.mozilla.org/en-US/docs/Web/API/Performance/now 148 | [`Performance.timeOrigin`]: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin 149 | [`Performance` object]: https://developer.mozilla.org/en-US/docs/Web/API/performance_property 150 | [`serde::Deserialize`]: https://docs.rs/serde/1/serde/trait.Deserialize.html 151 | [`serde::Serialize`]: https://docs.rs/serde/1/serde/trait.Serialize.html 152 | [`wasm-bindgen`]: https://crates.io/crates/wasm-bindgen 153 | [`f64.nearest`]: 154 | https://webassembly.github.io/spec/core/syntax/instructions.html#syntax-instr-numeric 155 | [`f64.trunc`]: https://webassembly.github.io/spec/core/syntax/instructions.html#syntax-instr-numeric 156 | -------------------------------------------------------------------------------- /tests/serde.rs: -------------------------------------------------------------------------------- 1 | //! [`serde`] tests for [`SystemTime`]. 2 | 3 | #![cfg(test)] 4 | #![cfg_attr(target_arch = "wasm32", no_main)] 5 | #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std)] 6 | 7 | extern crate alloc; 8 | 9 | mod util; 10 | 11 | use alloc::string::ToString; 12 | #[cfg(feature = "std")] 13 | use std::time::{Duration, SystemTime as StdSystemTime}; 14 | 15 | use serde_test::Token; 16 | use wasm_bindgen_test::wasm_bindgen_test; 17 | use web_time::SystemTime; 18 | 19 | /// De/Serialization of [`SystemTime`]. 20 | #[wasm_bindgen_test(unsupported = test)] 21 | fn system_time_json() { 22 | let time = SystemTime::now(); 23 | let serialized = serde_json::to_string(&time).unwrap(); 24 | let deserialized: SystemTime = serde_json::from_str(&serialized).unwrap(); 25 | assert_eq!(time, deserialized); 26 | } 27 | 28 | /// De/Serialization of [`SystemTime`] with 29 | /// [`UNIX_EPOCH`](SystemTime::UNIX_EPOCH). 30 | #[wasm_bindgen_test(unsupported = test)] 31 | fn unix_epoch_json() { 32 | let time = SystemTime::UNIX_EPOCH; 33 | let serialized = serde_json::to_string(&time).unwrap(); 34 | let deserialized: SystemTime = serde_json::from_str(&serialized).unwrap(); 35 | assert_eq!(time, deserialized); 36 | } 37 | 38 | /// De/Serialization compatibility with [`std::time::SystemTime`]. 39 | #[cfg(feature = "std")] 40 | #[wasm_bindgen_test(unsupported = test)] 41 | fn std_compatibility_json() { 42 | let time = SystemTime::now(); 43 | let serialized = serde_json::to_string(&time).unwrap(); 44 | let deserialized: StdSystemTime = serde_json::from_str(&serialized).unwrap(); 45 | assert_eq!( 46 | time.duration_since(SystemTime::UNIX_EPOCH).unwrap(), 47 | deserialized 48 | .duration_since(StdSystemTime::UNIX_EPOCH) 49 | .unwrap() 50 | ); 51 | 52 | let time = StdSystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000); 53 | let serialized = serde_json::to_string(&time).unwrap(); 54 | let deserialized: SystemTime = serde_json::from_str(&serialized).unwrap(); 55 | assert_eq!( 56 | time.duration_since(StdSystemTime::UNIX_EPOCH).unwrap(), 57 | deserialized.duration_since(SystemTime::UNIX_EPOCH).unwrap() 58 | ); 59 | } 60 | 61 | /// Deserialization from a sequence. 62 | #[wasm_bindgen_test(unsupported = test)] 63 | fn sequence() { 64 | serde_test::assert_de_tokens::( 65 | &SystemTime::UNIX_EPOCH, 66 | &[ 67 | Token::Seq { len: Some(2) }, 68 | Token::U64(0), 69 | Token::U32(0), 70 | Token::SeqEnd, 71 | ], 72 | ); 73 | } 74 | 75 | /// Deserialization from a map. 76 | #[wasm_bindgen_test(unsupported = test)] 77 | fn map() { 78 | serde_test::assert_de_tokens::( 79 | &SystemTime::UNIX_EPOCH, 80 | &[ 81 | Token::Map { len: Some(2) }, 82 | Token::Str("secs_since_epoch"), 83 | Token::U64(0), 84 | Token::Str("nanos_since_epoch"), 85 | Token::U32(0), 86 | Token::MapEnd, 87 | ], 88 | ); 89 | 90 | serde_test::assert_de_tokens::( 91 | &SystemTime::UNIX_EPOCH, 92 | &[ 93 | Token::Map { len: Some(2) }, 94 | Token::Bytes(b"secs_since_epoch"), 95 | Token::U64(0), 96 | Token::Bytes(b"nanos_since_epoch"), 97 | Token::U32(0), 98 | Token::MapEnd, 99 | ], 100 | ); 101 | } 102 | 103 | /// Deserialization failures from a sequence. 104 | #[wasm_bindgen_test(unsupported = test)] 105 | fn failure_sequence() { 106 | serde_test::assert_de_tokens_error::( 107 | &[Token::Seq { len: Some(0) }, Token::SeqEnd], 108 | "invalid length 0, expected struct SystemTime", 109 | ); 110 | 111 | serde_test::assert_de_tokens_error::( 112 | &[Token::Seq { len: Some(1) }, Token::Unit], 113 | "invalid type: unit value, expected u64", 114 | ); 115 | 116 | serde_test::assert_de_tokens_error::( 117 | &[Token::Seq { len: Some(1) }, Token::U64(0), Token::SeqEnd], 118 | "invalid length 1, expected struct SystemTime", 119 | ); 120 | 121 | serde_test::assert_de_tokens_error::( 122 | &[Token::Seq { len: Some(2) }, Token::U64(0), Token::Unit], 123 | "invalid type: unit value, expected u32", 124 | ); 125 | 126 | serde_test::assert_de_tokens_error::( 127 | &[ 128 | Token::Seq { len: Some(2) }, 129 | Token::U64(u64::MAX), 130 | Token::U32(u32::MAX), 131 | Token::SeqEnd, 132 | ], 133 | "overflow deserializing SystemTime epoch offset", 134 | ); 135 | } 136 | 137 | /// Deserialization failures from a map. 138 | #[wasm_bindgen_test(unsupported = test)] 139 | fn failure_map() { 140 | serde_test::assert_de_tokens_error::( 141 | &[Token::Map { len: Some(1) }, Token::Unit], 142 | "invalid type: unit value, expected `secs_since_epoch` or `nanos_since_epoch`", 143 | ); 144 | 145 | serde_test::assert_de_tokens_error::( 146 | &[Token::Map { len: Some(1) }, Token::Str("test")], 147 | "unknown field `test`, expected `secs_since_epoch` or `nanos_since_epoch`", 148 | ); 149 | 150 | serde_test::assert_de_tokens_error::( 151 | &[Token::Map { len: Some(1) }, Token::Bytes(b"test")], 152 | "unknown field `test`, expected `secs_since_epoch` or `nanos_since_epoch`", 153 | ); 154 | 155 | serde_test::assert_de_tokens_error::( 156 | &[ 157 | Token::Map { len: Some(2) }, 158 | Token::Str("secs_since_epoch"), 159 | Token::U64(0), 160 | Token::Str("secs_since_epoch"), 161 | ], 162 | "duplicate field `secs_since_epoch`", 163 | ); 164 | 165 | serde_test::assert_de_tokens_error::( 166 | &[ 167 | Token::Map { len: Some(2) }, 168 | Token::Str("nanos_since_epoch"), 169 | Token::U64(0), 170 | Token::Str("nanos_since_epoch"), 171 | ], 172 | "duplicate field `nanos_since_epoch`", 173 | ); 174 | 175 | serde_test::assert_de_tokens_error::( 176 | &[ 177 | Token::Map { len: Some(1) }, 178 | Token::Str("nanos_since_epoch"), 179 | Token::U64(0), 180 | Token::MapEnd, 181 | ], 182 | "missing field `secs_since_epoch`", 183 | ); 184 | 185 | serde_test::assert_de_tokens_error::( 186 | &[ 187 | Token::Map { len: Some(1) }, 188 | Token::Str("secs_since_epoch"), 189 | Token::U64(0), 190 | Token::MapEnd, 191 | ], 192 | "missing field `nanos_since_epoch`", 193 | ); 194 | 195 | serde_test::assert_de_tokens_error::( 196 | &[ 197 | Token::Map { len: Some(1) }, 198 | Token::Str("secs_since_epoch"), 199 | Token::Unit, 200 | Token::MapEnd, 201 | ], 202 | "invalid type: unit value, expected u64", 203 | ); 204 | 205 | serde_test::assert_de_tokens_error::( 206 | &[ 207 | Token::Map { len: Some(1) }, 208 | Token::Str("nanos_since_epoch"), 209 | Token::Unit, 210 | Token::MapEnd, 211 | ], 212 | "invalid type: unit value, expected u32", 213 | ); 214 | 215 | serde_test::assert_de_tokens_error::( 216 | &[ 217 | Token::Map { len: Some(2) }, 218 | Token::Str("secs_since_epoch"), 219 | Token::U64(u64::MAX), 220 | Token::Str("nanos_since_epoch"), 221 | Token::U32(u32::MAX), 222 | Token::MapEnd, 223 | ], 224 | "overflow deserializing SystemTime epoch offset", 225 | ); 226 | } 227 | 228 | /// Serializing failures. 229 | #[wasm_bindgen_test(unsupported = test)] 230 | fn failure_serialize() { 231 | let mut serialized = [0; 0]; 232 | let error = 233 | serde_json_core::to_slice(&SystemTime::UNIX_EPOCH, serialized.as_mut()).unwrap_err(); 234 | 235 | assert_eq!(error.to_string(), "Buffer is full"); 236 | 237 | let mut serialized = [0; 1]; 238 | let error = 239 | serde_json_core::to_slice(&SystemTime::UNIX_EPOCH, serialized.as_mut()).unwrap_err(); 240 | 241 | assert_eq!(error.to_string(), "Buffer is full"); 242 | 243 | let mut serialized = [0; 21]; 244 | let error = 245 | serde_json_core::to_slice(&SystemTime::UNIX_EPOCH, serialized.as_mut()).unwrap_err(); 246 | 247 | assert_eq!(error.to_string(), "Buffer is full"); 248 | } 249 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref_name }} 10 | cancel-in-progress: true 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | clippy: 17 | name: 18 | Clippy ${{ matrix.target.description }} ${{ matrix.rust.description }} ${{ 19 | matrix.features.description }} 20 | 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | target: 27 | - { target: x86_64-unknown-linux-gnu, description: Native } 28 | - { target: wasm32-unknown-unknown, description: Web } 29 | rust: 30 | - { version: stable, atomics: false } 31 | - { version: nightly, atomics: false } 32 | - { 33 | version: nightly, 34 | description: with Atomics, 35 | atomics: true, 36 | component: ",rust-src", 37 | cflags: -matomics -mbulk-memory, 38 | flags: "-Ctarget-feature=+atomics,+bulk-memory", 39 | build-std: true, 40 | } 41 | features: 42 | - { features: "", native: true, description: (`default`) } 43 | - { features: --features serde, native: false, description: (`default` `serde`) } 44 | - { features: --no-default-features --features std, native: false, description: (`std`) } 45 | - { features: --no-default-features, no_std: true, native: true, description: (`no_std`) } 46 | - { 47 | features: "--no-default-features --features msrv,serde", 48 | no_std: true, 49 | description: "(`no_std`, `msrv`, `serde`)", 50 | } 51 | - { 52 | features: "--no-default-features --features msrv,serde", 53 | no_std: true, 54 | nightly: true, 55 | description: "Nightly (`no_std`, `msrv`, `serde`)", 56 | } 57 | exclude: 58 | - target: { target: x86_64-unknown-linux-gnu, description: Native } 59 | rust: { atomics: true } 60 | - target: { target: x86_64-unknown-linux-gnu, description: Native } 61 | features: { native: false } 62 | - rust: { version: nightly, atomics: false } 63 | features: { nightly: false } 64 | - features: { nightly: true } 65 | rust: { version: stable } 66 | - rust: { atomics: true } 67 | features: { nightly: true } 68 | 69 | steps: 70 | - name: Checkout 71 | uses: actions/checkout@v4 72 | - name: Install Rust 73 | run: | 74 | rustup toolchain install ${{ matrix.rust.version }} --profile minimal --component clippy${{ matrix.rust.component }} --allow-downgrade --target ${{ matrix.target.target }} 75 | rustup default ${{ matrix.rust.version }} 76 | - name: Set `build-std` components 77 | if: matrix.rust.build-std == true && matrix.features.no_std == false 78 | run: echo "BUILD_STD_COMPONENTS=-Zbuild-std=panic_abort,std" >> $GITHUB_ENV 79 | - name: Set `build-std` `no_std` components 80 | if: matrix.rust.build-std == true && matrix.features.no_std == true 81 | run: echo "BUILD_STD_COMPONENTS=-Zbuild-std=core,alloc" >> $GITHUB_ENV 82 | - name: Run Clippy 83 | env: 84 | CFLAGS_wasm32_unknown_unknown: ${{ matrix.rust.cflags }} 85 | CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS: ${{ matrix.rust.flags }} 86 | run: 87 | cargo clippy --workspace --all-targets ${{ matrix.features.features }} --target ${{ 88 | matrix.target.target }} $BUILD_STD_COMPONENTS -- -D warnings 89 | 90 | rustdoc: 91 | name: 92 | Rustdoc ${{ matrix.target.description }} ${{ matrix.rust.description }} ${{ 93 | matrix.features.description }} ${{ matrix.docsrs.description }} 94 | 95 | runs-on: ubuntu-latest 96 | 97 | strategy: 98 | fail-fast: false 99 | matrix: 100 | target: 101 | - { target: x86_64-unknown-linux-gnu, description: Native } 102 | - { target: wasm32-unknown-unknown, description: Web } 103 | rust: 104 | - { version: stable, atomics: false } 105 | - { version: nightly, atomics: false } 106 | - { 107 | version: nightly, 108 | description: with Atomics, 109 | atomics: true, 110 | component: --component rust-src, 111 | cflags: -matomics -mbulk-memory, 112 | flags: "-Ctarget-feature=+atomics,+bulk-memory", 113 | build-std: true, 114 | } 115 | features: 116 | - { features: "", native: true, description: (`default`) } 117 | - { features: --features serde, description: (`default` `serde`) } 118 | - { features: --no-default-features --features std, description: (`std`) } 119 | - { features: --no-default-features, no_std: true, native: true, description: (`no_std`) } 120 | - { 121 | features: "--no-default-features --features msrv,serde", 122 | no_std: true, 123 | description: "(`no_std`, `msrv`, `serde`)", 124 | } 125 | - { 126 | features: "--no-default-features --features msrv,serde", 127 | no_std: true, 128 | nightly: true, 129 | description: "Nightly (`no_std`, `msrv`, `serde`)", 130 | } 131 | docsrs: 132 | - { flags: "", nightly: false } 133 | - { description: (docs.rs), flags: --cfg=docsrs, nightly: true } 134 | exclude: 135 | - docsrs: { flags: "", nightly: false } 136 | rust: { version: nightly, atomics: false } 137 | features: { nightly: false } 138 | - docsrs: { flags: --cfg=docsrs, nightly: true } 139 | rust: { version: stable } 140 | - target: { target: x86_64-unknown-linux-gnu, description: Native } 141 | rust: { atomics: true } 142 | - target: { target: x86_64-unknown-linux-gnu, description: Native } 143 | features: { native: false } 144 | - features: { nightly: true } 145 | rust: { version: stable } 146 | - rust: { atomics: true } 147 | features: { nightly: true } 148 | 149 | steps: 150 | - name: Checkout 151 | uses: actions/checkout@v4 152 | - name: Install Rust 153 | run: | 154 | rustup toolchain install ${{ matrix.rust.version }} --profile minimal ${{ matrix.rust.component }} --target ${{ matrix.target.target }} 155 | rustup default ${{ matrix.rust.version }} 156 | - name: Set `build-std` components 157 | if: matrix.rust.build-std == true && matrix.features.no_std == false 158 | run: echo "BUILD_STD_COMPONENTS=-Zbuild-std=panic_abort,std" >> $GITHUB_ENV 159 | - name: Set `build-std` `no_std` components 160 | if: matrix.rust.build-std == true && matrix.features.no_std == true 161 | run: echo "BUILD_STD_COMPONENTS=-Zbuild-std=core,alloc" >> $GITHUB_ENV 162 | - name: Run Rustdoc 163 | env: 164 | CFLAGS_wasm32_unknown_unknown: ${{ matrix.rust.cflags }} 165 | CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS: ${{ matrix.rust.flags }} 166 | RUSTDOCFLAGS: -D warnings ${{ matrix.rust.flags }} ${{ matrix.docsrs.flags }} 167 | run: 168 | cargo doc --workspace --no-deps --document-private-items --lib --examples ${{ 169 | matrix.features.features }} --target ${{ matrix.target.target }} $BUILD_STD_COMPONENTS 170 | 171 | file-permissions: 172 | name: File Permissions 173 | 174 | runs-on: ubuntu-latest 175 | 176 | steps: 177 | - name: Checkout 178 | uses: actions/checkout@v4 179 | - name: List all files that are executable 180 | run: find -type f -executable ! -path './.git/*' 181 | - name: Fail if any executable files were found 182 | run: find -type f -executable ! -path './.git/*' -exec false {} + 183 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Description 2 | //! 3 | //! Complete drop-in replacement for [`std::time`] that works in browsers. 4 | //! 5 | //! Currently [`Instant::now()`] and [`SystemTime::now()`] will simply panic 6 | //! when using the `wasm32-unknown-unknown` target. This implementation uses 7 | //! [`Performance.now()`] for [`Instant`] and [`Date.now()`] for [`SystemTime`] 8 | //! to offer a drop-in replacement that works in browsers. 9 | //! 10 | //! At the same time the library will simply re-export [`std::time`] when not 11 | //! using the `wasm32-unknown-unknown` or `wasm32v1-none` target and will not 12 | //! pull in any dependencies. 13 | //! 14 | //! Additionally, if compiled with `target-feature = "atomics"` it will 15 | //! synchronize the timestamps to account for different context's, like web 16 | //! workers. See [`Performance.timeOrigin`] for more information. 17 | //! 18 | //! # Target 19 | //! 20 | //! This library specifically targets browsers, that support 21 | //! [`Performance.now()`], with the `wasm32-unknown-unknown` or `wasm32v1-none` 22 | //! target. Emscripten is not supported. WASI doesn't require support as it has 23 | //! it's own native API to deal with [`std::time`]. 24 | //! 25 | //! Furthermore it depends on [`wasm-bindgen`], which is required. This library 26 | //! will continue to depend on it until a viable alternative presents itself, in 27 | //! which case multiple ecosystems could be supported. 28 | //! 29 | //! # Note 30 | //! 31 | //! ## Ticking during sleep 32 | //! 33 | //! Currently a known bug is affecting browsers on operating system other then 34 | //! Windows. This bug prevents [`Instant`] from continuing to tick when the 35 | //! context is asleep. While this doesn't conflict with Rusts requirements of 36 | //! [`Instant`], by chance Rust's Std 37 | //! [has the same problem](https://github.com/rust-lang/rust/issues/79462). 38 | //! 39 | //! See [the MDN documentation on this](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#ticking_during_sleep) for more information. 40 | //! 41 | //! ## Context support 42 | //! 43 | //! The implementation of [`Instant::now()`] relies on the availability of the 44 | //! [`Performance` object], a lack thereof will cause a panic. This can happen 45 | //! if called from a [worklet]. 46 | //! 47 | //! # Usage 48 | //! 49 | //! You can simply import the types you need: 50 | //! ``` 51 | //! # #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std, no_main)] 52 | //! # 53 | //! use web_time::{Instant, SystemTime}; 54 | //! # #[cfg(target_arch = "wasm32")] 55 | //! # use tests_web as _; 56 | //! # 57 | //! # #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 58 | //! # fn main() { 59 | //! let now = Instant::now(); 60 | //! let time = SystemTime::now(); 61 | //! # } 62 | //! ``` 63 | //! 64 | //! Using `-Ctarget-feature=+nontrapping-fptoint` will improve the performance 65 | //! of [`Instant::now()`] and [`SystemTime::now()`], but the vast majority of 66 | //! the time is still spent going through JS. 67 | //! 68 | //! # Features 69 | //! 70 | //! ## `std` (enabled by default) 71 | //! 72 | //! Enables the corresponding crate feature in all dependencies and allows for 73 | //! some optimized instruction output. 74 | //! 75 | //! Without this crate feature compilation the standard library is not included. 76 | //! Has no effect on targets other then `wasm32-unknown-unknown` or 77 | //! `wasm32v1-none`. 78 | //! 79 | //! ## `msrv` (enabled by default) 80 | //! 81 | //! Allows `web-time` to make use of features only available in higher MSRVs. 82 | //! This offers compile-time detection and does not break compilation when 83 | //! enabled with the crates MSRV. 84 | //! 85 | //! - Rust v1.77 + `std`: Enables the use of the [`f64.nearest`] instruction. 86 | //! Which will significantly reduce the instruction count for 87 | //! [`Instant::now()`]. 88 | //! - Rust Nightly: Enables the use of the [`f64.trunc`] and [`f64.nearest`] 89 | //! instruction. Which will significantly reduce the instruction count for 90 | //! [`Instant::now()`]. 91 | //! 92 | //! ## `serde` 93 | //! 94 | //! Implements [`serde::Deserialize`] and [`serde::Serialize`] for 95 | //! [`SystemTime`]. 96 | //! 97 | //! # Conditional Configurations 98 | //! 99 | //! ## `docsrs` 100 | //! 101 | //! This requires Rust nightly and enhances the documentation. It must only be 102 | //! used with `RUSTDOCFLAGS`, not with `RUSTFLAGS`. 103 | //! 104 | //! # MSRV Policy 105 | //! 106 | //! The MSRV is v1.60. Changes to the MSRV will be accompanied by a minor 107 | //! version bump. 108 | //! 109 | //! # Contributing 110 | //! 111 | //! See the [CONTRIBUTING] file for details. 112 | //! 113 | //! # Attribution 114 | //! 115 | //! Inspiration was taken from the [instant](https://github.com/sebcrozet/instant/tree/v0.1.12) project. 116 | //! 117 | //! Additional insight was taken from the [time](https://github.com/time-rs/time/tree/v0.3.20) project. 118 | //! 119 | //! # Changelog 120 | //! 121 | //! See the [CHANGELOG] file for details. 122 | //! 123 | //! # License 124 | //! 125 | //! Licensed under either of 126 | //! 127 | //! - Apache License, Version 2.0 ([LICENSE-APACHE] or ) 128 | //! - MIT license ([LICENSE-MIT] or ) 129 | //! 130 | //! at your option. 131 | //! 132 | //! ## Copyright 133 | //! 134 | //! A majority of the code and documentation was taken from [`std::time`]. For 135 | //! license information see [#License](https://github.com/rust-lang/rust/tree/1.68.1#license). 136 | //! 137 | //! ## Contribution 138 | //! 139 | //! Unless you explicitly state otherwise, any contribution intentionally 140 | //! submitted for inclusion in the work by you, as defined in the Apache-2.0 141 | //! license, shall be dual licensed as above, without any additional terms or 142 | //! conditions. 143 | //! 144 | //! [CHANGELOG]: https://github.com/daxpedda/web-time/blob/v1.1.0/CHANGELOG.md 145 | //! [CONTRIBUTING]: https://github.com/daxpedda/web-time/blob/v1.1.0/CONTRIBUTING.md 146 | //! [LICENSE-MIT]: https://github.com/daxpedda/web-time/blob/v1.1.0/LICENSE-MIT 147 | //! [LICENSE-APACHE]: https://github.com/daxpedda/web-time/blob/v1.1.0/LICENSE-APACHE 148 | //! [worklet]: https://developer.mozilla.org/en-US/docs/Web/API/Worklet 149 | //! [`Date.now()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now 150 | //! [`Instant`]: https://doc.rust-lang.org/std/time/struct.Instant.html 151 | //! [`Instant::now()`]: https://doc.rust-lang.org/std/time/struct.Instant.html#method.now 152 | //! [`SystemTime`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html 153 | //! [`SystemTime::now()`]: https://doc.rust-lang.org/std/time/struct.SystemTime.html#method.now 154 | //! [`std::time`]: https://doc.rust-lang.org/std/time/ 155 | //! [`performance.now()`]: https://developer.mozilla.org/en-US/docs/Web/API/Performance/now 156 | //! [`Performance.timeOrigin`]: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin 157 | //! [`Performance` object]: https://developer.mozilla.org/en-US/docs/Web/API/performance_property 158 | #![cfg_attr( 159 | any(not(feature = "serde"), not(target_arch = "wasm32")), 160 | doc = "[`serde::Deserialize`]: https://docs.rs/serde/1/serde/trait.Deserialize.html", 161 | doc = "[`serde::Serialize`]: https://docs.rs/serde/1/serde/trait.Serialize.html" 162 | )] 163 | //! [`wasm-bindgen`]: https://crates.io/crates/wasm-bindgen 164 | //! [`f64.nearest`]: https://webassembly.github.io/spec/core/syntax/instructions.html#syntax-instr-numeric 165 | //! [`f64.trunc`]: https://webassembly.github.io/spec/core/syntax/instructions.html#syntax-instr-numeric 166 | 167 | #![cfg_attr(all(target_arch = "wasm32", not(feature = "std")), no_std)] 168 | #![cfg_attr(all(test, target_arch = "wasm32"), no_main)] 169 | #![cfg_attr(all(doc, docsrs), feature(doc_cfg))] 170 | #![cfg_attr(all(not(feature = "std"), nightly), feature(asm_experimental_arch))] 171 | 172 | #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))] 173 | mod time; 174 | #[cfg(any( 175 | all( 176 | target_arch = "wasm32", 177 | any(target_os = "unknown", target_os = "none"), 178 | feature = "std" 179 | ), 180 | all(doc, docsrs) 181 | ))] 182 | #[cfg_attr(all(doc, docsrs), doc(cfg(all(Web, feature = "std"))))] 183 | pub mod web; 184 | 185 | #[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))] 186 | pub use std::time::*; 187 | 188 | #[cfg(all(test, target_arch = "wasm32"))] 189 | use tests_web as _; 190 | 191 | #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))] 192 | pub use self::time::*; 193 | 194 | #[cfg(all(not(doc), docsrs))] 195 | compile_error!("`--cfg docsrs` must only be used via `RUSTDOCFLAGS`, not `RUSTFLAGS`"); 196 | 197 | #[cfg(all(test, not(target_arch = "wasm32")))] 198 | fn main() {} 199 | -------------------------------------------------------------------------------- /.github/workflows/coverage-documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Coverage & Documentation 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref_name }} 11 | cancel-in-progress: true 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | coverage: 18 | name: Test Coverage ${{ matrix.mt.description }} ${{ matrix.features.description }} 19 | 20 | runs-on: ubuntu-latest 21 | 22 | timeout-minutes: 10 23 | 24 | strategy: 25 | matrix: 26 | mt: 27 | - { id: "st" } 28 | - { 29 | id: "mt", 30 | description: with Atomics, 31 | component: --component rust-src, 32 | cflags: -matomics -mbulk-memory, 33 | flags: "-Ctarget-feature=+atomics,+bulk-memory", 34 | build-std: true, 35 | } 36 | features: 37 | - { id: "", features: "", no_std: false } 38 | - { id: -no_std, features: --no-default-features, no_std: true, description: (`no_std`) } 39 | 40 | env: 41 | CFLAGS_wasm32_unknown_unknown: ${{ matrix.mt.cflags }} 42 | CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS: 43 | -Cinstrument-coverage -Zcoverage-options=condition -Zno-profiler-runtime --emit=llvm-ir 44 | --cfg=web_time_test_coverage ${{ matrix.mt.flags }} 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | - name: Install `wasm-bindgen-cli` 50 | uses: taiki-e/cache-cargo-install-action@v2 51 | with: 52 | tool: wasm-bindgen-cli 53 | git: https://github.com/daxpedda/wasm-bindgen 54 | rev: d4cb4a5d94090c18b469796250744612fd347dbd 55 | - name: Install Clang v19 56 | run: | 57 | wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - 58 | sudo add-apt-repository "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-19 main" 59 | sudo apt-get install clang-19 60 | - name: Install Rust nightly 61 | run: | 62 | rustup toolchain install nightly --profile minimal --target wasm32-unknown-unknown ${{ matrix.mt.component }} 63 | rustup default nightly 64 | - name: Set `build-std` components 65 | if: matrix.mt.build-std == true && matrix.features.no_std == false 66 | run: echo "BUILD_STD_COMPONENTS=-Zbuild-std=panic_abort,std" >> $GITHUB_ENV 67 | - name: Set `build-std` `no_std` components 68 | if: matrix.mt.build-std == true && matrix.features.no_std == true 69 | run: echo "BUILD_STD_COMPONENTS=-Zbuild-std=core,alloc" >> $GITHUB_ENV 70 | - name: Test 71 | env: 72 | WASM_BINDGEN_USE_BROWSER: 1 73 | CHROMEDRIVER_REMOTE: http://127.0.0.1:9000 74 | run: | 75 | chromedriver --port=9000 & 76 | mkdir coverage-output 77 | LLVM_PROFILE_FILE=$(realpath coverage-output)/%m_%p.profraw cargo test --workspace --features serde --target wasm32-unknown-unknown $BUILD_STD_COMPONENTS ${{ matrix.features.features }} --tests 78 | - name: Prepare Object Files 79 | run: | 80 | mkdir coverage-input 81 | crate_name=web_time 82 | IFS=$'\n' 83 | for file in $( 84 | cargo test --workspace --features serde --target wasm32-unknown-unknown $BUILD_STD_COMPONENTS ${{ matrix.features.features }} --tests --no-run --message-format=json | \ 85 | jq -r "select(.reason == \"compiler-artifact\") | (select(.target.kind == [\"test\"]) // select(.target.name == \"$crate_name\")) | .filenames[0]" 86 | ) 87 | do 88 | if [[ ${file##*.} == "rlib" ]]; then 89 | base=$(basename $file .rlib) 90 | file=$(dirname $file)/${base#"lib"}.ll 91 | else 92 | file=$(dirname $file)/$(basename $file .wasm).ll 93 | fi 94 | 95 | clang-19 $file -Wno-override-module -c -o coverage-output/$(basename $file .ll).o 96 | done 97 | - name: Upload Test Coverage Artifact 98 | uses: actions/upload-artifact@v4 99 | with: 100 | name: test-coverage-${{ matrix.mt.id }}${{ matrix.features.id }} 101 | path: coverage-output 102 | retention-days: 1 103 | if-no-files-found: error 104 | 105 | collect-coverage: 106 | name: Collect Test Coverage 107 | 108 | needs: coverage 109 | 110 | runs-on: ubuntu-latest 111 | 112 | steps: 113 | - name: Checkout 114 | uses: actions/checkout@v4 115 | - name: Install Rust nightly 116 | run: | 117 | rustup toolchain install nightly --profile minimal --component llvm-tools 118 | rustup default nightly 119 | - name: Install `cargo-binutils` 120 | uses: taiki-e/install-action@v2 121 | with: 122 | tool: cargo-binutils 123 | - name: Download Test Coverage 124 | uses: actions/download-artifact@v4 125 | with: 126 | pattern: test-coverage-* 127 | path: coverage-input 128 | - name: Merge Profile Data 129 | run: 130 | rust-profdata merge -sparse coverage-input/*/*.profraw -o coverage-input/coverage.profdata 131 | - name: Export Code Coverage Report 132 | run: | 133 | mkdir coverage-output 134 | objects=() 135 | for file in $(ls coverage-input/*/*.o) 136 | do 137 | objects+=(-object $file) 138 | done 139 | rust-cov show -show-instantiations=false -output-dir coverage-output -format=html -instr-profile=coverage-input/coverage.profdata ${objects[@]} -sources src 140 | rust-cov export -format=text -summary-only -instr-profile=coverage-input/coverage.profdata ${objects[@]} -sources src | \ 141 | printf '{ "coverage": "%.2f%%" }' $(jq '.data[0].totals.functions.percent') > coverage-output/coverage.json 142 | sed 's///' coverage-output/index.html | sed "s/