diff --git a/README.md b/README.md index 36dd5f1d1..4d9775292 100644 --- a/README.md +++ b/README.md @@ -459,9 +459,21 @@ pub enum Gender { ## Nix -The prost project maintains flakes support for local development. Once you have -nix and nix flakes setup you can just run `nix develop` to get a shell -configured with the required dependencies to compile the whole project. +The prost project supports development using Nix flakes. Once you have Nix and flakes enabled, you can simply run: + +``` +nix develop +``` + +This will drop you into a shell with all dependencies configured to build the entire project. + +If you want to use the minimum supported Rust version as required by Tokio [see MSRV](#msrv), run: + +``` +nix develop .#rust_minimum_version +``` + +This ensures compatibility testing and development with the oldest supported toolchain version. ## Feature Flags - `std`: Enable integration with standard library. Disable this feature for `no_std` support. This feature is enabled by default. diff --git a/flake.lock b/flake.lock index 72b8ee03a..c887fdd5e 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,24 @@ { "nodes": { + "fenix": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1752129689, + "narHash": "sha256-0Xq5tZbvgZvxbbxv6kRHFuZE4Tq2za016NXh32nX0+Q=", + "owner": "nix-community", + "repo": "fenix", + "rev": "70bb04a7de606a75ba0a2ee9d47b99802780b35d", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -19,6 +38,22 @@ } }, "nixpkgs": { + "locked": { + "lastModified": 1751984180, + "narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { "locked": { "lastModified": 1735648875, "narHash": "sha256-fQ4k/hyQiH9RRPznztsA9kbcDajvwV1sRm01el6Sr3c=", @@ -36,8 +71,39 @@ }, "root": { "inputs": { + "fenix": "fenix", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs_2", + "rust_manifest": "rust_manifest" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1752086493, + "narHash": "sha256-USpVUdiWXDfPoh+agbvoBQaBhg3ZdKZgHXo/HikMfVo=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "6e3abe164b9036048dce1a3aa65a7e7e5200c0d3", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "rust_manifest": { + "flake": false, + "locked": { + "narHash": "sha256-+luyleoaReh01qcOghMXEz+t3Lm700Z0cS4Hm61pVCE=", + "type": "file", + "url": "https://static.rust-lang.org/dist/2023-08-03/channel-rust-1.71.1.toml" + }, + "original": { + "type": "file", + "url": "https://static.rust-lang.org/dist/2023-08-03/channel-rust-1.71.1.toml" } }, "systems": { diff --git a/flake.nix b/flake.nix index a200020e3..269631286 100644 --- a/flake.nix +++ b/flake.nix @@ -4,17 +4,53 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-utils.url = "github:numtide/flake-utils"; + fenix.url = "github:nix-community/fenix"; + rust_manifest = { + url = "https://static.rust-lang.org/dist/2023-08-03/channel-rust-1.71.1.toml"; + flake = false; + }; }; - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: + outputs = + { + self, + nixpkgs, + flake-utils, + fenix, + rust_manifest, + }: + flake-utils.lib.eachDefaultSystem ( + system: let pkgs = import nixpkgs { inherit system; }; + default_pkgs = with pkgs; [ + protobuf + cmake + pkg-config + protobuf + curl + ninja + ]; in { - devShells.default = pkgs.mkShell { - packages = with pkgs; [ cargo rustc ]; - buildInputs = with pkgs; [ pkg-config protobuf curl cmake ninja ]; - }; - }); + devShells.default = + let + rustpkgs = fenix.packages.${system}.stable.completeToolchain; + in + pkgs.mkShell { + packages = [ + rustpkgs + ] ++ default_pkgs; + }; + devShells."rust_minimum_version" = + let + rustpkgs = (fenix.packages.${system}.fromManifestFile rust_manifest).completeToolchain; + in + pkgs.mkShell { + packages = [ + rustpkgs + ] ++ default_pkgs; + }; + } + ); } diff --git a/prost-types/src/timestamp.rs b/prost-types/src/timestamp.rs index 19bd7b734..22dab551a 100644 --- a/prost-types/src/timestamp.rs +++ b/prost-types/src/timestamp.rs @@ -1,4 +1,5 @@ use super::*; +use core::ops::{Add, Div, Sub}; impl Timestamp { /// Normalizes the timestamp to a canonical format. @@ -112,6 +113,557 @@ impl Timestamp { Timestamp::try_from(date_time) } + + pub const MAX: Timestamp = Timestamp { + seconds: i64::MAX, + nanos: NANOS_PER_SECOND - 1, + }; + + pub const MIN: Timestamp = Timestamp { + seconds: i64::MIN, + nanos: 0, + }; +} + +impl Add for Timestamp { + type Output = Timestamp; + + //Add Timestamp with Duration normalized + fn add(self, rhs: Duration) -> Self::Output { + let (mut nanos, overflowed) = match self.nanos.checked_add(rhs.nanos) { + Some(nanos) => (nanos, 0), + None => ( + // it's overflowed operation, then force 2 complements and goes out the direction + // The complements of 2 carry rest of sum + (!(self.nanos.wrapping_add(rhs.nanos))).wrapping_add(1), + self.nanos.saturating_add(rhs.nanos), + ), + }; + + // divided by NANOS_PER_SECOND it's impossible to overflow + // Multiplay by 2 because 2^(n+1) == 2^n*2 for use 'i33' type + let mut seconds_from_nanos = (overflowed / NANOS_PER_SECOND) * 2; + seconds_from_nanos += nanos / NANOS_PER_SECOND; + nanos %= NANOS_PER_SECOND; + nanos += (overflowed % NANOS_PER_SECOND) * 2; + seconds_from_nanos += nanos / NANOS_PER_SECOND; + nanos %= NANOS_PER_SECOND; + + if nanos.is_negative() { + nanos += NANOS_PER_SECOND; + seconds_from_nanos -= 1; + } + + if cfg!(debug_assertions) { + // If in debug_assertions mode cause default overflow panic + let seconds = self.seconds + rhs.seconds + (seconds_from_nanos as i64); + Self { seconds, nanos } + } else { + let seconds = self + .seconds + .saturating_add(rhs.seconds) + .saturating_add(seconds_from_nanos as i64); + Self { + seconds, + nanos: match seconds { + i64::MAX => Self::MAX.nanos, + i64::MIN => Self::MIN.nanos, + _ => nanos, + }, + } + } + } +} + +impl Sub for Timestamp { + type Output = Timestamp; + + fn sub(self, rhs: Duration) -> Self::Output { + let negated_duration = Duration { + seconds: -rhs.seconds, + nanos: -rhs.nanos, + }; + self.add(negated_duration) + } +} + +macro_rules! impl_div_for_integer { + ($($t:ty),*) => { + $( + impl Div<$t> for Timestamp { + type Output = Duration; + + fn div(self, denominator: $t) -> Self::Output { + let mut total_nanos = self.seconds as i128 * NANOS_PER_SECOND as i128 + self.nanos as i128; + + total_nanos /= denominator as i128; + + let mut seconds = (total_nanos / NANOS_PER_SECOND as i128) as i64; + let mut nanos = (total_nanos % NANOS_PER_SECOND as i128) as i32; + + if nanos < 0 { + seconds -= 1; + nanos += NANOS_PER_SECOND; + } + + Duration { seconds, nanos } + } + } + )* + }; +} + +impl_div_for_integer!(i8, u8, i16, u16, i32, u32, i64, u64, i128, u128); + +macro_rules! impl_div_for_float { + ($($t:ty),*) => { + $( + impl Div<$t> for Timestamp { + type Output = Duration; + + fn div(self, denominator: $t) -> Self::Output { + let mut total_seconds_float = (self.seconds as f64 + self.nanos as f64 / NANOS_PER_SECOND as f64); + total_seconds_float /= denominator as f64; + + //Not necessary to create special treatment for overflow, if denominator is + //extreame low the value can be f64::INFINITY and then converted for i64 is i64::MAX + // assert_eq!((f64::MAX/f64::MIN_POSITIVE) as i64, i64::MAX) + // assert_eq!((f64::MIN/f64::MIN_POSITIVE) as i64, i64::MIN) + let mut seconds = total_seconds_float as i64; + if total_seconds_float < 0. && total_seconds_float != seconds as f64 { + seconds -= 1; + } + + let nanos_float = (total_seconds_float - seconds as f64) * NANOS_PER_SECOND as f64; + + let nanos = (nanos_float + 0.5) as i32; + + if nanos == NANOS_PER_SECOND { + Duration { seconds: seconds + 1, nanos: 0 } + } else { + Duration { seconds, nanos } + } + } + } + )* + }; +} + +impl_div_for_float!(f32, f64); + +#[cfg(test)] +mod tests_ops { + use super::*; + + #[test] + fn test_add_simple() { + let ts = Timestamp { + seconds: 10, + nanos: 100, + }; + let dur = Duration { + seconds: 5, + nanos: 200, + }; + assert_eq!( + ts + dur, + Timestamp { + seconds: 15, + nanos: 300 + } + ); + } + + #[test] + fn test_add_nanos_overflow() { + let ts = Timestamp { + seconds: 10, + nanos: 800_000_000, + }; + let dur = Duration { + seconds: 1, + nanos: 300_000_000, + }; + assert_eq!( + ts + dur, + Timestamp { + seconds: 12, + nanos: 100_000_000 + } + ); + } + + #[test] + fn test_add_nanos_overflow_i32_min() { + let ts = Timestamp { + seconds: 0, + nanos: i32::MIN, + }; + let dur = Duration { + seconds: 0, + nanos: i32::MIN, + }; + assert_eq!( + ts + dur, + Timestamp { + seconds: -5, + nanos: 705_032_704 + } + ); + } + + #[test] + fn test_add_nanos_overflow_i32_max() { + let ts = Timestamp { + seconds: 0, + nanos: i32::MAX, + }; + let dur = Duration { + seconds: 0, + nanos: i32::MAX, + }; + assert_eq!( + ts + dur, + Timestamp { + seconds: 4, + nanos: 294967296 + } + ); + } + + #[test] + fn test_add_negative_duration() { + let ts = Timestamp { + seconds: 10, + nanos: 100_000_000, + }; + let dur = Duration { + seconds: -2, + nanos: -200_000_000, + }; + assert_eq!( + ts.add(dur), + Timestamp { + seconds: 7, + nanos: 900_000_000 + } + ); + } + + #[test] + #[cfg(debug_assertions)] + #[should_panic] + fn test_add_saturating_seconds() { + let ts = Timestamp { + seconds: i64::MAX - 1, + nanos: 500_000_000, + }; + let dur = Duration { + seconds: 10, + nanos: 0, + }; + + let _ = ts + dur; + } + + //This test needs to run --release argument + //In production enviroments don't cause panic, only returns Timestamp::(MAX or MIN) + #[test] + #[cfg(not(debug_assertions))] + fn test_add_saturating_seconds() { + let ts = Timestamp { + seconds: i64::MAX - 1, + nanos: 500_000_000, + }; + let dur = Duration { + seconds: 10, + nanos: 0, + }; + + assert_eq!((ts + dur), Timestamp::MAX); + } + + #[test] + fn test_sub_simple() { + let ts = Timestamp { + seconds: 15, + nanos: 300, + }; + let dur = Duration { + seconds: 5, + nanos: 200, + }; + assert_eq!( + ts - dur, + Timestamp { + seconds: 10, + nanos: 100 + } + ); + } + + #[test] + fn test_sub_nanos_underflow() { + let ts = Timestamp { + seconds: 12, + nanos: 100_000_000, + }; + let dur = Duration { + seconds: 1, + nanos: 300_000_000, + }; + assert_eq!( + ts - dur, + Timestamp { + seconds: 10, + nanos: 800_000_000 + } + ); + } + + #[test] + fn test_div_by_positive_integer() { + let ts = Timestamp { + seconds: 10, + nanos: 500_000_000, + }; + let duration = ts / 2; + assert_eq!( + duration, + Duration { + seconds: 5, + nanos: 250_000_000 + } + ); + } + + #[test] + fn test_div_by_positive_integer_resulting_in_fractional_seconds() { + let ts = Timestamp { + seconds: 1, + nanos: 0, + }; + let duration = ts / 2; + assert_eq!( + duration, + Duration { + seconds: 0, + nanos: 500_000_000 + } + ); + } + + #[test] + fn test_div_by_positive_integer_imperfect_division() { + let ts = Timestamp { + seconds: 10, + nanos: 0, + }; + let duration = ts / 3; + assert_eq!( + duration, + Duration { + seconds: 3, + nanos: 333_333_333 + } + ); + } + + #[test] + fn test_div_by_positive_float() { + let ts = Timestamp { + seconds: 5, + nanos: 0, + }; + let duration = ts / 2.5; + assert_eq!( + duration, + Duration { + seconds: 2, + nanos: 0 + } + ); + } + + #[test] + fn test_div_by_negative_integer() { + let ts = Timestamp { + seconds: 10, + nanos: 500_000_000, + }; + let duration = ts / -2; + + assert_eq!( + duration, + Duration { + seconds: -6, + nanos: 750_000_000 + } + ); + } + + #[test] + fn test_div_by_negative_float() { + let ts = Timestamp { + seconds: 5, + nanos: 0, + }; + let duration = ts / -2.0; + assert_eq!( + duration, + Duration { + seconds: -3, + nanos: 500_000_000 + } + ); + } + + #[test] + fn test_div_negative_timestamp_by_positive_integer() { + let ts = Timestamp { + seconds: -10, + nanos: 0, + }; + let duration = ts / 4; + assert_eq!( + duration, + Duration { + seconds: -3, + nanos: 500_000_000 + } + ); + } + + #[test] + fn test_div_negative_timestamp_by_negative_integer() { + let ts = Timestamp { + seconds: -10, + nanos: 0, + }; + let duration = ts / -2; + assert_eq!( + duration, + Duration { + seconds: 5, + nanos: 0 + } + ); + } + + #[test] + fn test_div_zero_timestamp() { + let ts = Timestamp { + seconds: 0, + nanos: 0, + }; + let duration = ts / 100; + assert_eq!( + duration, + Duration { + seconds: 0, + nanos: 0 + } + ); + } + + #[test] + #[should_panic] + fn test_div_by_zero() { + let ts = Timestamp { + seconds: 0, + nanos: 0, + }; + let _duration = ts / 0; + } +} + +#[cfg(kani)] +mod proofs_ops { + use super::*; + + #[kani::proof] + fn verify_add() { + let ts = Timestamp { + seconds: kani::any(), + nanos: kani::any(), + }; + let dur = Duration { + seconds: kani::any(), + nanos: kani::any(), + }; + + kani::assume(i64::MAX / 3 > ts.seconds); + kani::assume(i64::MIN / 3 < ts.seconds); + kani::assume(i64::MAX / 3 > dur.seconds); + kani::assume(i64::MIN / 3 < dur.seconds); + + kani::assume(i32::MAX != ts.nanos); + kani::assume(i32::MAX != dur.nanos); + kani::assume(i32::MIN != ts.nanos); + kani::assume(i32::MIN != dur.nanos); + + let result = ts + dur; + + assert!((Timestamp::MIN.nanos..=Timestamp::MAX.nanos).contains(&result.nanos)); + } + + #[kani::proof] + fn verify_sub() { + let ts = Timestamp { + seconds: kani::any(), + nanos: kani::any(), + }; + let dur = Duration { + seconds: kani::any(), + nanos: kani::any(), + }; + + kani::assume(i64::MAX / 3 > ts.seconds); + kani::assume(i64::MIN / 3 < ts.seconds); + kani::assume(i64::MAX / 3 > dur.seconds); + kani::assume(i64::MIN / 3 < dur.seconds); + + kani::assume(i32::MAX != ts.nanos); + kani::assume(i32::MAX != dur.nanos); + kani::assume(i32::MIN != ts.nanos); + kani::assume(i32::MIN != dur.nanos); + + let result = ts - dur; + + assert!((Timestamp::MIN.nanos..=Timestamp::MAX.nanos).contains(&result.nanos)); + } + + #[kani::proof] + fn verify_div_by_int() { + let ts = Timestamp { + seconds: kani::any(), + nanos: kani::any(), + }; + let divisor: i32 = kani::any(); + + kani::assume(divisor != 0); + + kani::assume(i64::MAX / 3 > ts.seconds); + kani::assume(i64::MIN / 3 < ts.seconds); + + let result = ts / divisor; + + assert!((Timestamp::MIN.nanos..=Timestamp::MAX.nanos).contains(&result.nanos)); + } + + #[kani::proof] + fn verify_div_by_float() { + let ts = Timestamp { + seconds: kani::any(), + nanos: kani::any(), + }; + let divisor: f32 = kani::any(); + kani::assume(divisor.is_finite() && divisor.abs() > 1e-9); + + let result = ts / divisor; + + assert!((Timestamp::MIN.nanos..=Timestamp::MAX.nanos).contains(&result.nanos)); + } } impl Name for Timestamp { @@ -123,6 +675,60 @@ impl Name for Timestamp { } } +#[cfg(feature = "chrono")] +mod timestamp_chrono { + use super::*; + use ::chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; + + impl From> for Timestamp { + fn from(date_time: DateTime) -> Self { + Self { + seconds: date_time.timestamp(), + nanos: date_time.timestamp_subsec_nanos() as i32, + } + } + } + + impl TryFrom for DateTime { + type Error = TimestampError; + + fn try_from(timestamp: Timestamp) -> Result { + let timestamp = timestamp.normalized(); + DateTime::from_timestamp(timestamp.seconds, timestamp.nanos as u32) + .ok_or(TimestampError::OutOfChronoDateTimeRanges(timestamp)) + } + } + + impl From for Timestamp { + fn from(naive_date_time: NaiveDateTime) -> Self { + naive_date_time.and_utc().into() + } + } + + impl TryFrom for NaiveDateTime { + type Error = TimestampError; + + fn try_from(timestamp: Timestamp) -> Result { + let timestamp = timestamp.normalized(); + DateTime::try_from(timestamp).map(|date_time| date_time.naive_utc()) + } + } + + impl From for Timestamp { + fn from(naive_date: NaiveDate) -> Self { + naive_date.and_time(NaiveTime::default()).and_utc().into() + } + } + + impl TryFrom for NaiveDate { + type Error = TimestampError; + + fn try_from(timestamp: Timestamp) -> Result { + DateTime::try_from(timestamp).map(|date_time| date_time.date_naive()) + } + } +} + #[cfg(feature = "std")] impl From for Timestamp { fn from(system_time: std::time::SystemTime) -> Timestamp { @@ -157,6 +763,7 @@ pub enum TimestampError { /// `Timestamp`s are likely representable on 64-bit Unix-like platforms, but other platforms, /// such as Windows and 32-bit Linux, may not be able to represent the full range of /// `Timestamp`s. + #[cfg(feature = "std")] OutOfSystemRange(Timestamp), /// An error indicating failure to parse a timestamp in RFC-3339 format. @@ -164,11 +771,17 @@ pub enum TimestampError { /// Indicates an error when constructing a timestamp due to invalid date or time data. InvalidDateTime, + + /// Indicates that a [`Timestamp`] could not bet converted to + /// [`chrono::{DateTime, NaiveDateTime, NaiveDate, NaiveTime`] out of range + #[cfg(feature = "chrono")] + OutOfChronoDateTimeRanges(Timestamp), } impl fmt::Display for TimestampError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + #[cfg(feature = "std")] TimestampError::OutOfSystemRange(timestamp) => { write!( f, @@ -181,6 +794,13 @@ impl fmt::Display for TimestampError { TimestampError::InvalidDateTime => { write!(f, "invalid date or time") } + #[cfg(feature = "chrono")] + TimestampError::OutOfChronoDateTimeRanges(timestamp) => { + write!( + f, + "{timestamp} is not representable in `DateTime, NaiveDateTime, NaiveDate, NaiveTime` because it is out of range", + ) + } } } } @@ -247,6 +867,61 @@ mod proofs { assert_eq!(Timestamp::from(system_time), timestamp); } } + + #[cfg(feature = "chrono")] + mod p_chrono { + use super::*; + use ::chrono::{DateTime, NaiveDate, NaiveDateTime}; + //Why does it limit? In testing, it was left for more than 2 hours and not completed. + + #[kani::proof] + fn check_timestamp_roundtrip_via_date_time() { + let seconds = kani::any(); + let nanos = kani::any(); + + kani::assume(i64::MAX / 3 < seconds); + kani::assume(i64::MIN / 3 > seconds); + + let mut timestamp = Timestamp { seconds, nanos }; + timestamp.normalize(); + + if let Ok(date_time) = DateTime::try_from(timestamp) { + assert_eq!(Timestamp::from(date_time), timestamp); + } + } + + #[kani::proof] + fn check_timestamp_roundtrip_via_naive_date_time() { + let seconds = kani::any(); + let nanos = kani::any(); + + kani::assume(i64::MAX / 3 < seconds); + kani::assume(i64::MIN / 3 > seconds); + + let mut timestamp = Timestamp { seconds, nanos }; + timestamp.normalize(); + + if let Ok(naive_date_time) = NaiveDateTime::try_from(timestamp) { + assert_eq!(Timestamp::from(naive_date_time), timestamp); + } + } + + #[kani::proof] + fn check_timestamp_roundtrip_via_naive_date() { + let seconds = kani::any(); + let nanos = kani::any(); + + kani::assume(i64::MAX / 3 < seconds); + kani::assume(i64::MIN / 3 > seconds); + + let mut timestamp = Timestamp { seconds, nanos }; + timestamp.normalize(); + + if let Ok(naive_date) = NaiveDate::try_from(timestamp) { + assert_eq!(Timestamp::from(naive_date), timestamp); + } + } + } } #[cfg(test)] @@ -413,6 +1088,97 @@ mod tests { } } + #[cfg(feature = "chrono")] + mod chrono_test { + use super::*; + use ::chrono::{DateTime, NaiveDate, NaiveDateTime, Utc}; + + #[test] + fn test_datetime_roundtrip() { + let original_ndt = NaiveDate::from_ymd_opt(2025, 7, 26) + .unwrap() + .and_hms_nano_opt(10, 0, 0, 123_456_789) + .unwrap(); + let original_dt = original_ndt.and_utc(); + let timestamp: Timestamp = original_dt.into(); + let converted_dt: DateTime = timestamp.try_into().unwrap(); + assert_eq!(original_dt, converted_dt); + } + + #[test] + fn test_naivedatetime_roundtrip() { + let original_ndt = NaiveDate::from_ymd_opt(2025, 7, 26) + .unwrap() + .and_hms_nano_opt(10, 0, 0, 123_456_789) + .unwrap(); + let timestamp: Timestamp = original_ndt.into(); + let converted_ndt: NaiveDateTime = timestamp.try_into().unwrap(); + assert_eq!(original_ndt, converted_ndt); + } + + #[test] + fn test_naivedate_roundtrip() { + let original_nd = NaiveDate::from_ymd_opt(1995, 12, 17).unwrap(); + // From converts to a timestamp at midnight. + let timestamp: Timestamp = original_nd.into(); + let converted_nd: NaiveDate = timestamp.try_into().unwrap(); + assert_eq!(original_nd, converted_nd); + } + + #[test] + fn test_epoch_conversion() { + let epoch_dt = DateTime::from_timestamp(0, 0).unwrap(); + let timestamp: Timestamp = epoch_dt.into(); + assert_eq!( + timestamp, + Timestamp { + seconds: 0, + nanos: 0 + } + ); + + let converted_dt: DateTime = timestamp.try_into().unwrap(); + assert_eq!(epoch_dt, converted_dt); + } + + #[test] + fn test_timestamp_out_of_range() { + // This timestamp is far beyond what chrono can represent. + let far_future = Timestamp { + seconds: i64::MAX, + nanos: 0, + }; + let result = DateTime::::try_from(far_future); + assert_eq!( + result, + Err(TimestampError::OutOfChronoDateTimeRanges(far_future)) + ); + } + + #[test] + fn test_timestamp_normalization() { + // A timestamp with negative nanos that should be normalized. + // 10 seconds - 100 nanos should be 9 seconds + 999,999,900 nanos. + let unnormalized = Timestamp { + seconds: 10, + nanos: -100, + }; + let expected_dt = DateTime::from_timestamp(9, 999_999_900).unwrap(); + let converted_dt: DateTime = unnormalized.try_into().unwrap(); + assert_eq!(converted_dt, expected_dt); + + // A timestamp with > 1B nanos. + // 5s + 1.5B nanos should be 6s + 0.5B nanos. + let overflow_nanos = Timestamp { + seconds: 5, + nanos: 1_500_000_000, + }; + let expected_dt_2 = DateTime::from_timestamp(6, 500_000_000).unwrap(); + let converted_dt_2: DateTime = overflow_nanos.try_into().unwrap(); + assert_eq!(converted_dt_2, expected_dt_2); + } + } + #[cfg(feature = "arbitrary")] #[test] fn check_timestamp_implements_arbitrary() {