From 7da3a41f8c5118e149a0498f39a82592ba57a77a Mon Sep 17 00:00:00 2001 From: Ethan Chou Date: Thu, 26 Mar 2026 16:08:11 -0400 Subject: [PATCH 1/3] add impl for generating durations and instants --- src/generators/default.rs | 14 +++- src/generators/mod.rs | 2 + src/generators/time.rs | 137 ++++++++++++++++++++++++++++++++++++++ tests/test_time.rs | 38 +++++++++++ 4 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 src/generators/time.rs create mode 100644 tests/test_time.rs diff --git a/src/generators/default.rs b/src/generators/default.rs index 0d8e4ec7..24995361 100644 --- a/src/generators/default.rs +++ b/src/generators/default.rs @@ -1,10 +1,11 @@ use super::{ - BoolGenerator, BoxedGenerator, FloatGenerator, Generator, HashMapGenerator, IntegerGenerator, - OptionalGenerator, TextGenerator, VecGenerator, booleans, collections::ArrayGenerator, floats, - hashmaps, integers, optional, text, vecs, + BoolGenerator, BoxedGenerator, DurationGenerator, FloatGenerator, Generator, HashMapGenerator, + IntegerGenerator, OptionalGenerator, TextGenerator, VecGenerator, booleans, + collections::ArrayGenerator, durations, floats, hashmaps, integers, optional, text, vecs, }; use std::collections::HashMap; use std::hash::Hash; +use std::time::Duration; /// Trait for types that have a default generator. /// @@ -187,6 +188,13 @@ where } } +impl DefaultGenerator for Duration { + type Generator = DurationGenerator; + fn default_generator() -> Self::Generator { + durations() + } +} + impl DefaultGenerator for HashMap where diff --git a/src/generators/mod.rs b/src/generators/mod.rs index fe7b970b..94d90961 100644 --- a/src/generators/mod.rs +++ b/src/generators/mod.rs @@ -15,6 +15,7 @@ mod generators; mod misc; mod numeric; mod strings; +mod time; mod tuples; #[cfg(feature = "rand")] @@ -52,6 +53,7 @@ pub use strings::{ IpAddressGenerator, RegexGenerator, TextGenerator, TimeGenerator, UrlGenerator, binary, dates, datetimes, domains, emails, from_regex, ip_addresses, text, times, urls, }; +pub use time::{DurationGenerator, InstantGenerator, durations, instants}; #[doc(hidden)] pub use tuples::{ tuples0, tuples1, tuples2, tuples3, tuples4, tuples5, tuples6, tuples7, tuples8, tuples9, diff --git a/src/generators/time.rs b/src/generators/time.rs new file mode 100644 index 00000000..b7d8b291 --- /dev/null +++ b/src/generators/time.rs @@ -0,0 +1,137 @@ +use super::{BasicGenerator, Generator, TestCase}; +use crate::cbor_utils::cbor_map; +use std::time::{Duration, Instant}; + +/// Generator for [`Duration`] values. Created by [`durations()`]. +/// +/// Internally generates nanoseconds as a `u64`, so the maximum representable +/// duration is approximately 584 years (`u64::MAX` nanoseconds). +/// Use `min_value` and `max_value` to constrain the range. +pub struct DurationGenerator { + min_nanos: u64, + max_nanos: u64, +} + +impl DurationGenerator { + /// Set the minimum duration (inclusive). + pub fn min_value(mut self, min: Duration) -> Self { + self.min_nanos = duration_to_nanos(min); + self + } + + /// Set the maximum duration (inclusive). + pub fn max_value(mut self, max: Duration) -> Self { + self.max_nanos = duration_to_nanos(max); + self + } + + fn build_schema(&self) -> ciborium::Value { + assert!( + self.min_nanos <= self.max_nanos, + "Cannot have max_value < min_value" + ); + cbor_map! { + "type" => "integer", + "min_value" => self.min_nanos, + "max_value" => self.max_nanos + } + } +} + +impl Generator for DurationGenerator { + fn do_draw(&self, tc: &TestCase) -> Duration { + let nanos: u64 = super::generate_from_schema(tc, &self.build_schema()); + Duration::from_nanos(nanos) + } + + fn as_basic(&self) -> Option> { + Some(BasicGenerator::new(self.build_schema(), |raw| { + let nanos: u64 = super::deserialize_value(raw); + Duration::from_nanos(nanos) + })) + } +} + +/// Generate [`Duration`] values. +/// +/// By default, generates durations from zero up to `u64::MAX` nanoseconds +/// (approximately 584 years). Use `min_value` and `max_value` to constrain +/// the range. +/// +/// # Example +/// +/// ```no_run +/// use std::time::Duration; +/// +/// #[hegel::test] +/// fn my_test(tc: hegel::TestCase) { +/// let d = tc.draw(hegel::generators::durations() +/// .max_value(Duration::from_secs(60))); +/// assert!(d <= Duration::from_secs(60)); +/// } +/// ``` +pub fn durations() -> DurationGenerator { + DurationGenerator { + min_nanos: 0, + max_nanos: u64::MAX, + } +} + +/// Generator for [`Instant`] values. Created by [`instants()`]. +/// +/// Generates instants by adding a random [`Duration`] offset to the current +/// time (`Instant::now()`). Since `Instant` values are inherently tied to the +/// monotonic clock, each test run produces different absolute values. +pub struct InstantGenerator { + max_offset_nanos: u64, +} + +impl InstantGenerator { + /// Set the maximum offset from `Instant::now()` (inclusive). + pub fn max_offset(mut self, max: Duration) -> Self { + self.max_offset_nanos = duration_to_nanos(max); + self + } +} + +impl Generator for InstantGenerator { + fn do_draw(&self, tc: &TestCase) -> Instant { + let schema = cbor_map! { + "type" => "integer", + "min_value" => 0u64, + "max_value" => self.max_offset_nanos + }; + let nanos: u64 = super::generate_from_schema(tc, &schema); + Instant::now() + Duration::from_nanos(nanos) + } +} + +/// Generate [`Instant`] values. +/// +/// Produces instants offset from `Instant::now()` by a random duration. +/// The default maximum offset is one hour. Use `max_offset` to change it. +/// +/// Since `Instant` is tied to the monotonic clock, generated values differ +/// between test runs. This generator is most useful for testing code that +/// computes differences between instants. +/// +/// # Example +/// +/// ```no_run +/// use std::time::Duration; +/// +/// #[hegel::test] +/// fn my_test(tc: hegel::TestCase) { +/// let i = tc.draw(hegel::generators::instants() +/// .max_offset(Duration::from_secs(3600))); +/// } +/// ``` +pub fn instants() -> InstantGenerator { + InstantGenerator { + max_offset_nanos: 3_600_000_000_000, + } +} + +fn duration_to_nanos(d: Duration) -> u64 { + d.as_nanos().try_into().unwrap_or(u64::MAX) +} diff --git a/tests/test_time.rs b/tests/test_time.rs new file mode 100644 index 00000000..f7e73251 --- /dev/null +++ b/tests/test_time.rs @@ -0,0 +1,38 @@ +mod common; + +use common::utils::assert_all_examples; +use hegel::generators; +use std::time::{Duration, Instant}; + +#[test] +fn test_durations_default() { + assert_all_examples(generators::durations(), |d| *d >= Duration::ZERO); +} + +#[test] +fn test_durations_bounded() { + let min = Duration::from_secs(5); + let max = Duration::from_secs(60); + assert_all_examples( + generators::durations().min_value(min).max_value(max), + move |d| *d >= min && *d <= max, + ); +} + +#[test] +fn test_instants_default() { + let before = Instant::now(); + let max_offset = Duration::from_secs(3600); + assert_all_examples(generators::instants(), move |i| { + *i >= before && *i <= Instant::now() + max_offset + }); +} + +#[test] +fn test_instants_bounded() { + let max_offset = Duration::from_secs(10); + let before = Instant::now(); + assert_all_examples(generators::instants().max_offset(max_offset), move |i| { + *i >= before && *i <= Instant::now() + max_offset + }); +} From 4b02ae09118ccaf6e58934941a2cf61b1a3535b1 Mon Sep 17 00:00:00 2001 From: Ethan Chou Date: Thu, 26 Mar 2026 16:14:57 -0400 Subject: [PATCH 2/3] fix instant base for determinism/shrink --- RELEASE.md | 3 +++ src/generators/time.rs | 23 +++++++++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..2bfe024c --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +Add generators for Duration and Instant \ No newline at end of file diff --git a/src/generators/time.rs b/src/generators/time.rs index b7d8b291..4fd4243b 100644 --- a/src/generators/time.rs +++ b/src/generators/time.rs @@ -79,15 +79,16 @@ pub fn durations() -> DurationGenerator { /// Generator for [`Instant`] values. Created by [`instants()`]. /// -/// Generates instants by adding a random [`Duration`] offset to the current -/// time (`Instant::now()`). Since `Instant` values are inherently tied to the -/// monotonic clock, each test run produces different absolute values. +/// Generates instants by adding a random [`Duration`] offset to a fixed base +/// instant captured when the generator is created. The offsets are deterministic +/// (controlled by the test engine), while the base varies between runs. pub struct InstantGenerator { + base: Instant, max_offset_nanos: u64, } impl InstantGenerator { - /// Set the maximum offset from `Instant::now()` (inclusive). + /// Set the maximum offset from the base instant (inclusive). pub fn max_offset(mut self, max: Duration) -> Self { self.max_offset_nanos = duration_to_nanos(max); self @@ -102,18 +103,19 @@ impl Generator for InstantGenerator { "max_value" => self.max_offset_nanos }; let nanos: u64 = super::generate_from_schema(tc, &schema); - Instant::now() + Duration::from_nanos(nanos) + self.base + Duration::from_nanos(nanos) } } /// Generate [`Instant`] values. /// -/// Produces instants offset from `Instant::now()` by a random duration. -/// The default maximum offset is one hour. Use `max_offset` to change it. +/// Produces instants offset from a fixed base (`Instant::now()` at call time) +/// by a random duration. The default maximum offset is one hour. Use +/// `max_offset` to change it. /// -/// Since `Instant` is tied to the monotonic clock, generated values differ -/// between test runs. This generator is most useful for testing code that -/// computes differences between instants. +/// The base is captured once when `instants()` is called, so all generated +/// values within a test share the same reference point. The offsets are +/// deterministic and shrinkable. /// /// # Example /// @@ -128,6 +130,7 @@ impl Generator for InstantGenerator { /// ``` pub fn instants() -> InstantGenerator { InstantGenerator { + base: Instant::now(), max_offset_nanos: 3_600_000_000_000, } } From 8313f29bc54372fffb794ade8f99e15a95510527 Mon Sep 17 00:00:00 2001 From: echoumcp1 <101858583+echoumcp1@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:15:00 -0400 Subject: [PATCH 3/3] remove instants generator --- RELEASE.md | 2 +- src/generators/mod.rs | 2 +- src/generators/time.rs | 60 +----------------------------------------- tests/test_time.rs | 20 +------------- 4 files changed, 4 insertions(+), 80 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 2bfe024c..87c725d8 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,3 +1,3 @@ RELEASE_TYPE: patch -Add generators for Duration and Instant \ No newline at end of file +Add generator for Duration \ No newline at end of file diff --git a/src/generators/mod.rs b/src/generators/mod.rs index 94d90961..3bc8b57b 100644 --- a/src/generators/mod.rs +++ b/src/generators/mod.rs @@ -53,7 +53,7 @@ pub use strings::{ IpAddressGenerator, RegexGenerator, TextGenerator, TimeGenerator, UrlGenerator, binary, dates, datetimes, domains, emails, from_regex, ip_addresses, text, times, urls, }; -pub use time::{DurationGenerator, InstantGenerator, durations, instants}; +pub use time::{DurationGenerator, durations}; #[doc(hidden)] pub use tuples::{ tuples0, tuples1, tuples2, tuples3, tuples4, tuples5, tuples6, tuples7, tuples8, tuples9, diff --git a/src/generators/time.rs b/src/generators/time.rs index 4fd4243b..f5b59e61 100644 --- a/src/generators/time.rs +++ b/src/generators/time.rs @@ -1,6 +1,6 @@ use super::{BasicGenerator, Generator, TestCase}; use crate::cbor_utils::cbor_map; -use std::time::{Duration, Instant}; +use std::time::Duration; /// Generator for [`Duration`] values. Created by [`durations()`]. /// @@ -77,64 +77,6 @@ pub fn durations() -> DurationGenerator { } } -/// Generator for [`Instant`] values. Created by [`instants()`]. -/// -/// Generates instants by adding a random [`Duration`] offset to a fixed base -/// instant captured when the generator is created. The offsets are deterministic -/// (controlled by the test engine), while the base varies between runs. -pub struct InstantGenerator { - base: Instant, - max_offset_nanos: u64, -} - -impl InstantGenerator { - /// Set the maximum offset from the base instant (inclusive). - pub fn max_offset(mut self, max: Duration) -> Self { - self.max_offset_nanos = duration_to_nanos(max); - self - } -} - -impl Generator for InstantGenerator { - fn do_draw(&self, tc: &TestCase) -> Instant { - let schema = cbor_map! { - "type" => "integer", - "min_value" => 0u64, - "max_value" => self.max_offset_nanos - }; - let nanos: u64 = super::generate_from_schema(tc, &schema); - self.base + Duration::from_nanos(nanos) - } -} - -/// Generate [`Instant`] values. -/// -/// Produces instants offset from a fixed base (`Instant::now()` at call time) -/// by a random duration. The default maximum offset is one hour. Use -/// `max_offset` to change it. -/// -/// The base is captured once when `instants()` is called, so all generated -/// values within a test share the same reference point. The offsets are -/// deterministic and shrinkable. -/// -/// # Example -/// -/// ```no_run -/// use std::time::Duration; -/// -/// #[hegel::test] -/// fn my_test(tc: hegel::TestCase) { -/// let i = tc.draw(hegel::generators::instants() -/// .max_offset(Duration::from_secs(3600))); -/// } -/// ``` -pub fn instants() -> InstantGenerator { - InstantGenerator { - base: Instant::now(), - max_offset_nanos: 3_600_000_000_000, - } -} - fn duration_to_nanos(d: Duration) -> u64 { d.as_nanos().try_into().unwrap_or(u64::MAX) } diff --git a/tests/test_time.rs b/tests/test_time.rs index f7e73251..b905c579 100644 --- a/tests/test_time.rs +++ b/tests/test_time.rs @@ -2,7 +2,7 @@ mod common; use common::utils::assert_all_examples; use hegel::generators; -use std::time::{Duration, Instant}; +use std::time::Duration; #[test] fn test_durations_default() { @@ -18,21 +18,3 @@ fn test_durations_bounded() { move |d| *d >= min && *d <= max, ); } - -#[test] -fn test_instants_default() { - let before = Instant::now(); - let max_offset = Duration::from_secs(3600); - assert_all_examples(generators::instants(), move |i| { - *i >= before && *i <= Instant::now() + max_offset - }); -} - -#[test] -fn test_instants_bounded() { - let max_offset = Duration::from_secs(10); - let before = Instant::now(); - assert_all_examples(generators::instants().max_offset(max_offset), move |i| { - *i >= before && *i <= Instant::now() + max_offset - }); -}