├── .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 | [](https://crates.io/crates/web-time)
4 | [](https://github.com/daxpedda/web-time/actions?query=branch%3Amain)
5 | [](https://docs.rs/web-time/1.1.0)
6 | [](https://daxpedda.github.io/web-time/doc/web_time)
7 | [](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/