From 5fd431d2a30ff2829283bab18f8e085578ec9b7d Mon Sep 17 00:00:00 2001 From: Lucas Alves de Lima Date: Fri, 11 Jul 2025 03:39:27 -0300 Subject: [PATCH 1/6] ci-cd(flake.nix): change to use fenix rust version manager and add another develop rust_minimum_version for tests --- flake.lock | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++- flake.nix | 49 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/flake.lock b/flake.lock index 72b8ee03a..0419426cb 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,26 @@ }, "root": { "inputs": { + "fenix": "fenix", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs_2" + } + }, + "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" } }, "systems": { diff --git a/flake.nix b/flake.nix index a200020e3..333d9402f 100644 --- a/flake.nix +++ b/flake.nix @@ -4,17 +4,52 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-utils.url = "github:numtide/flake-utils"; + fenix.url = "github:nix-community/fenix"; }; - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: + outputs = + { + self, + nixpkgs, + flake-utils, + fenix, + }: + 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.defaultToolchain; + in + pkgs.mkShell { + packages = [ + rustpkgs + ] ++ default_pkgs; + }; + devShells."rust_minimum_version" = + let + rust_manifest = { + url = "https://static.rust-lang.org/dist/2023-08-03/channel-rust-1.71.1.toml"; + flake = false; + }; + rustpkgs = (fenix.packages.${system}.fromManifestFile rust_manifest).defaultToolchain; + in + pkgs.mkShell { + packages = [ + rustpkgs + ] ++ default_pkgs; + }; + } + ); } From b07b0992823bb1bd9eb2d7de228c670c206fac1d Mon Sep 17 00:00:00 2001 From: Lucas Alves de Lima Date: Fri, 11 Jul 2025 03:40:33 -0300 Subject: [PATCH 2/6] feat(prost-types,timestamp): impl chrono types convert and unitary tests --- prost-types/src/timestamp.rs | 199 +++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/prost-types/src/timestamp.rs b/prost-types/src/timestamp.rs index 19bd7b734..b42df2bdd 100644 --- a/prost-types/src/timestamp.rs +++ b/prost-types/src/timestamp.rs @@ -123,6 +123,74 @@ impl Name for Timestamp { } } +#[cfg(feature = "chrono")] +mod 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()) + } + } + + impl From for Timestamp { + fn from(naive_time: NaiveTime) -> Self { + NaiveDate::default().and_time(naive_time).and_utc().into() + } + } + + impl TryFrom for NaiveTime { + type Error = TimestampError; + + fn try_from(timestamp: Timestamp) -> Result { + DateTime::try_from(timestamp).map(|date_time| date_time.time()) + } + } +} + #[cfg(feature = "std")] impl From for Timestamp { fn from(system_time: std::time::SystemTime) -> Timestamp { @@ -164,6 +232,11 @@ pub enum TimestampError { /// Indicates an error when constructing a timestamp due to invalid date or time data. InvalidDateTime, + + #[cfg(feature = "chrono")] + /// Indicates that a [`Timestamp`] could not bet converted to + /// [`chrono::{DateTime, NaiveDateTime, NaiveDate, NaiveTime`] out of range + OutOfChronoDateTimeRanges(Timestamp), } impl fmt::Display for TimestampError { @@ -181,6 +254,14 @@ 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 +328,124 @@ mod proofs { assert_eq!(Timestamp::from(system_time), timestamp); } } + + #[cfg(feature = "chrono")] + mod kani_chrono { + use super::*; + use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc}; + use std::convert::{TryFrom, TryInto}; + + #[kani::proof] + fn verify_from_datetime_utc() { + let date_time: chrono::DateTime = kani::any(); + let timestamp = Timestamp::from(date_time); + assert_eq!(timestamp.seconds, date_time.timestamp()); + assert_eq!(timestamp.nanos, date_time.timestamp_subsec_nanos() as i32); + } + + #[kani::proof] + fn verify_from_naive_datetime() { + let naive_dt: NaiveDateTime = kani::any(); + let timestamp = Timestamp::from(naive_dt); + let expected_dt_utc = naive_dt.and_utc(); + assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); + assert_eq!( + timestamp.nanos, + expected_dt_utc.timestamp_subsec_nanos() as i32 + ); + } + + #[kani::proof] + fn verify_from_naive_date() { + let naive_date: NaiveDate = kani::any(); + let timestamp = Timestamp::from(naive_date); + let naive_dt = naive_date.and_time(NaiveTime::default()); + let expected_dt_utc = naive_dt.and_utc(); + assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); + assert_eq!( + timestamp.nanos, + expected_dt_utc.timestamp_subsec_nanos() as i32 + ); + } + + #[kani::proof] + fn verify_from_naive_time() { + let naive_time: NaiveTime = kani::any(); + let timestamp = Timestamp::from(naive_time); + let naive_dt = NaiveDate::default().and_time(naive_time); + let expected_dt_utc = naive_dt.and_utc(); + assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); + assert_eq!( + timestamp.nanos, + expected_dt_utc.timestamp_subsec_nanos() as i32 + ); + } + + #[kani::proof] + fn verify_roundtrip_from_timestamp_to_datetime() { + let timestamp: Timestamp = kani::any(); + // Precondition: The timestamp must be valid according to its spec. + kani::assume((0..1_000_000_000).contains(×tamp.nanos)); + + if let Ok(dt_utc) = ::chrono::DateTime::::try_from(timestamp.clone()) { + // If conversion succeeds, the reverse must also succeed and be identical. + let roundtrip_timestamp = Timestamp::from(dt_utc); + assert_eq!(timestamp, roundtrip_timestamp); + } + } + + #[kani::proof] + fn verify_roundtrip_from_timestamp_to_naive_datetime() { + let timestamp: Timestamp = kani::any(); + kani::assume((0..1_000_000_000).contains(×tamp.nanos)); + + if let Ok(naive_dt) = ::chrono::NaiveDateTime::try_from(timestamp.clone()) { + let roundtrip_timestamp = Timestamp::from(naive_dt); + assert_eq!(timestamp, roundtrip_timestamp); + } + } + + #[kani::proof] + fn verify_roundtrip_from_timestamp_to_naive_date() { + let timestamp: Timestamp = kani::any(); + kani::assume((0..1_000_000_000).contains(×tamp.nanos)); + + if let Ok(naive_date) = ::chrono::NaiveDate::try_from(timestamp.clone()) { + let roundtrip_timestamp = Timestamp::from(naive_date); + + // The original timestamp, when converted, should match the round-tripped date. + let original_dt = ::chrono::DateTime::::try_from(timestamp).unwrap(); + assert_eq!(original_dt.date_naive(), naive_date); + + // The round-tripped timestamp should correspond to midnight of that day. + let expected_dt = naive_date.and_time(NaiveTime::default()).and_utc(); + assert_eq!(roundtrip_timestamp.seconds, expected_dt.timestamp()); + assert_eq!(roundtrip_timestamp.nanos, 0); + } + } + + #[kani::proof] + fn verify_roundtrip_from_timestamp_to_naive_time() { + let timestamp: Timestamp = kani::any(); + kani::assume((0..1_000_000_000).contains(×tamp.nanos)); + + if let Ok(naive_time) = ::chrono::NaiveTime::try_from(timestamp.clone()) { + let roundtrip_timestamp = Timestamp::from(naive_time); + + // The original timestamp's time part should match the converted naive_time. + let original_dt = ::chrono::DateTime::::try_from(timestamp).unwrap(); + assert_eq!(original_dt.time(), naive_time); + + // The round-tripped timestamp should correspond to the naive_time on the epoch date. + let expected_dt = NaiveDate::default().and_time(naive_time).and_utc(); + assert_eq!(roundtrip_timestamp.seconds, expected_dt.timestamp()); + assert_eq!( + roundtrip_timestamp.nanos, + expected_dt.timestamp_subsec_nanos() as i32 + ); + } + } + } } #[cfg(test)] From e50044cf14bf385638e53ae05e2a7904ada71d74 Mon Sep 17 00:00:00 2001 From: Lucas Alves de Lima Date: Sun, 20 Jul 2025 04:16:55 -0300 Subject: [PATCH 3/6] feat(prost-types,timestamp): Ops + Tests + Kani: - Implement Add, Sub and Div math operations; - Create unitary testes for Ops; - Create kani tests for ops; - Update kani tests for chrono conversions; - Add unitary testes for chrono conversions. --- prost-types/src/timestamp.rs | 920 ++++++++++++++++++++++++++++++----- 1 file changed, 808 insertions(+), 112 deletions(-) diff --git a/prost-types/src/timestamp.rs b/prost-types/src/timestamp.rs index b42df2bdd..61846a1b5 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. @@ -114,6 +115,541 @@ impl Timestamp { } } +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 => NANOS_PER_SECOND, + i64::MIN => 0, + _ => 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) + } +} + +//This can be more nice, if impl overflow controlled such use i128 +macro_rules! impl_div_for_integer { + ($($t:ty),*) => { + $( + impl Div<$t> for Timestamp { + type Output = Duration; + + fn div(self, rhs: $t) -> Self::Output { + let total_nanos = self.seconds as i128 * NANOS_PER_SECOND as i128 + self.nanos as i128; + + let result_nanos = total_nanos / (rhs as i128); + + let mut seconds = (result_nanos / NANOS_PER_SECOND as i128) as i64; + let mut nanos = (result_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, rhs: $t) -> Self::Output { + let total_seconds_float = (self.seconds as f64 + self.nanos as f64 / NANOS_PER_SECOND as f64) / rhs as f64; + + let mut seconds = total_seconds_float as i64; + if total_seconds_float < 0.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; + } + + #[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).seconds, i64::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 kani_verification_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!(result.nanos >= 0 && result.nanos < NANOS_PER_SECOND); + } + + #[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!(result.nanos >= 0 && result.nanos < NANOS_PER_SECOND); + } + + #[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!(result.nanos >= 0 && result.nanos < NANOS_PER_SECOND); + } + + #[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!(result.nanos >= 0 && result.nanos <= NANOS_PER_SECOND); + } +} + impl Name for Timestamp { const PACKAGE: &'static str = PACKAGE; const NAME: &'static str = "Timestamp"; @@ -189,6 +725,101 @@ mod chrono { DateTime::try_from(timestamp).map(|date_time| date_time.time()) } } + + #[cfg(test)] + mod tests { + use ::chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; + + #[test] + fn test_datetime_roundtrip() { + let original_dt = Utc::now(); + 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(2023, 10, 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_naivetime_roundtrip() { + let original_nt = NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap(); + // From converts to a timestamp on the default date (1970-01-01). + let timestamp: Timestamp = original_nt.into(); + let converted_nt: NaiveTime = timestamp.try_into().unwrap(); + assert_eq!(original_nt, converted_nt); + } + + #[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 = "std")] @@ -332,119 +963,184 @@ mod proofs { #[cfg(feature = "chrono")] mod kani_chrono { use super::*; - use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc}; - use std::convert::{TryFrom, TryInto}; - - #[kani::proof] - fn verify_from_datetime_utc() { - let date_time: chrono::DateTime = kani::any(); - let timestamp = Timestamp::from(date_time); - assert_eq!(timestamp.seconds, date_time.timestamp()); - assert_eq!(timestamp.nanos, date_time.timestamp_subsec_nanos() as i32); - } - - #[kani::proof] - fn verify_from_naive_datetime() { - let naive_dt: NaiveDateTime = kani::any(); - let timestamp = Timestamp::from(naive_dt); - let expected_dt_utc = naive_dt.and_utc(); - assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); - assert_eq!( - timestamp.nanos, - expected_dt_utc.timestamp_subsec_nanos() as i32 - ); - } - - #[kani::proof] - fn verify_from_naive_date() { - let naive_date: NaiveDate = kani::any(); - let timestamp = Timestamp::from(naive_date); - let naive_dt = naive_date.and_time(NaiveTime::default()); - let expected_dt_utc = naive_dt.and_utc(); - assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); - assert_eq!( - timestamp.nanos, - expected_dt_utc.timestamp_subsec_nanos() as i32 - ); - } + use ::chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc}; + //Why does it limit? In testing, it was left for more than 2 hours and not completed. - #[kani::proof] - fn verify_from_naive_time() { - let naive_time: NaiveTime = kani::any(); - let timestamp = Timestamp::from(naive_time); - let naive_dt = NaiveDate::default().and_time(naive_time); - let expected_dt_utc = naive_dt.and_utc(); - assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); - assert_eq!( - timestamp.nanos, - expected_dt_utc.timestamp_subsec_nanos() as i32 - ); - } - - #[kani::proof] - fn verify_roundtrip_from_timestamp_to_datetime() { - let timestamp: Timestamp = kani::any(); - // Precondition: The timestamp must be valid according to its spec. - kani::assume((0..1_000_000_000).contains(×tamp.nanos)); - - if let Ok(dt_utc) = ::chrono::DateTime::::try_from(timestamp.clone()) { - // If conversion succeeds, the reverse must also succeed and be identical. - let roundtrip_timestamp = Timestamp::from(dt_utc); - assert_eq!(timestamp, roundtrip_timestamp); - } - } - - #[kani::proof] - fn verify_roundtrip_from_timestamp_to_naive_datetime() { - let timestamp: Timestamp = kani::any(); - kani::assume((0..1_000_000_000).contains(×tamp.nanos)); - - if let Ok(naive_dt) = ::chrono::NaiveDateTime::try_from(timestamp.clone()) { - let roundtrip_timestamp = Timestamp::from(naive_dt); - assert_eq!(timestamp, roundtrip_timestamp); - } - } - - #[kani::proof] - fn verify_roundtrip_from_timestamp_to_naive_date() { - let timestamp: Timestamp = kani::any(); - kani::assume((0..1_000_000_000).contains(×tamp.nanos)); - - if let Ok(naive_date) = ::chrono::NaiveDate::try_from(timestamp.clone()) { - let roundtrip_timestamp = Timestamp::from(naive_date); - - // The original timestamp, when converted, should match the round-tripped date. - let original_dt = ::chrono::DateTime::::try_from(timestamp).unwrap(); - assert_eq!(original_dt.date_naive(), naive_date); - - // The round-tripped timestamp should correspond to midnight of that day. - let expected_dt = naive_date.and_time(NaiveTime::default()).and_utc(); - assert_eq!(roundtrip_timestamp.seconds, expected_dt.timestamp()); - assert_eq!(roundtrip_timestamp.nanos, 0); - } - } - - #[kani::proof] - fn verify_roundtrip_from_timestamp_to_naive_time() { - let timestamp: Timestamp = kani::any(); - kani::assume((0..1_000_000_000).contains(×tamp.nanos)); - - if let Ok(naive_time) = ::chrono::NaiveTime::try_from(timestamp.clone()) { - let roundtrip_timestamp = Timestamp::from(naive_time); - - // The original timestamp's time part should match the converted naive_time. - let original_dt = ::chrono::DateTime::::try_from(timestamp).unwrap(); - assert_eq!(original_dt.time(), naive_time); - - // The round-tripped timestamp should correspond to the naive_time on the epoch date. - let expected_dt = NaiveDate::default().and_time(naive_time).and_utc(); - assert_eq!(roundtrip_timestamp.seconds, expected_dt.timestamp()); - assert_eq!( - roundtrip_timestamp.nanos, - expected_dt.timestamp_subsec_nanos() as i32 - ); - } - } + // #[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); + // } + // } + + // #[kani::proof] + // fn check_timestamp_roundtrip_via_naive_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_time) = NaiveTime::try_from(timestamp) { + // assert_eq!(Timestamp::from(naive_time), timestamp); + // } + // } + + // #[kani::proof] + // fn verify_from_datetime_utc() { + // let date_time: DateTime = + // DateTime::from_timestamp(kani::any(), kani::any()).unwrap_or_default(); + // let timestamp = Timestamp::from(date_time); + // assert_eq!(timestamp.seconds, date_time.timestamp()); + // assert_eq!(timestamp.nanos, date_time.timestamp_subsec_nanos() as i32); + // } + + // #[kani::proof] + // fn verify_from_naive_datetime() { + // let naive_dt: NaiveDateTime = kani::any(); + // let timestamp = Timestamp::from(naive_dt); + // let expected_dt_utc = naive_dt.and_utc(); + // assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); + // assert_eq!( + // timestamp.nanos, + // expected_dt_utc.timestamp_subsec_nanos() as i32 + // ); + // } + // + // #[kani::proof] + // fn verify_from_naive_date() { + // let naive_date: NaiveDate = kani::any(); + // let timestamp = Timestamp::from(naive_date); + // let naive_dt = naive_date.and_time(NaiveTime::default()); + // let expected_dt_utc = naive_dt.and_utc(); + // assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); + // assert_eq!( + // timestamp.nanos, + // expected_dt_utc.timestamp_subsec_nanos() as i32 + // ); + // } + // + // #[kani::proof] + // fn verify_from_naive_time() { + // let naive_time: NaiveTime = kani::any(); + // let timestamp = Timestamp::from(naive_time); + // let naive_dt = NaiveDate::default().and_time(naive_time); + // let expected_dt_utc = naive_dt.and_utc(); + // assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); + // assert_eq!( + // timestamp.nanos, + // expected_dt_utc.timestamp_subsec_nanos() as i32 + // ); + // } + // + // #[kani::proof] + // fn verify_roundtrip_from_timestamp_to_datetime() { + // let timestamp: Timestamp = kani::any(); + // // Precondition: The timestamp must be valid according to its spec. + // kani::assume((0..1_000_000_000).contains(×tamp.nanos)); + // + // if let Ok(dt_utc) = ::chrono::DateTime::::try_from(timestamp.clone()) { + // // If conversion succeeds, the reverse must also succeed and be identical. + // let roundtrip_timestamp = Timestamp::from(dt_utc); + // assert_eq!(timestamp, roundtrip_timestamp); + // } + // } + // + // #[kani::proof] + // fn verify_roundtrip_from_timestamp_to_naive_datetime() { + // let timestamp: Timestamp = kani::any(); + // kani::assume((0..1_000_000_000).contains(×tamp.nanos)); + // + // if let Ok(naive_dt) = ::chrono::NaiveDateTime::try_from(timestamp.clone()) { + // let roundtrip_timestamp = Timestamp::from(naive_dt); + // assert_eq!(timestamp, roundtrip_timestamp); + // } + // } + // + // #[kani::proof] + // fn verify_roundtrip_from_timestamp_to_naive_date() { + // let timestamp: Timestamp = kani::any(); + // kani::assume((0..1_000_000_000).contains(×tamp.nanos)); + // + // if let Ok(naive_date) = ::chrono::NaiveDate::try_from(timestamp.clone()) { + // let roundtrip_timestamp = Timestamp::from(naive_date); + // + // // The original timestamp, when converted, should match the round-tripped date. + // let original_dt = ::chrono::DateTime::::try_from(timestamp).unwrap(); + // assert_eq!(original_dt.date_naive(), naive_date); + // + // // The round-tripped timestamp should correspond to midnight of that day. + // let expected_dt = naive_date.and_time(NaiveTime::default()).and_utc(); + // assert_eq!(roundtrip_timestamp.seconds, expected_dt.timestamp()); + // assert_eq!(roundtrip_timestamp.nanos, 0); + // } + // } + // + // #[kani::proof] + // fn verify_roundtrip_from_timestamp_to_naive_time() { + // let timestamp: Timestamp = kani::any(); + // kani::assume((0..1_000_000_000).contains(×tamp.nanos)); + // + // if let Ok(naive_time) = ::chrono::NaiveTime::try_from(timestamp.clone()) { + // let roundtrip_timestamp = Timestamp::from(naive_time); + // + // // The original timestamp's time part should match the converted naive_time. + // let original_dt = ::chrono::DateTime::::try_from(timestamp).unwrap(); + // assert_eq!(original_dt.time(), naive_time); + // + // // The round-tripped timestamp should correspond to the naive_time on the epoch date. + // let expected_dt = NaiveDate::default().and_time(naive_time).and_utc(); + // assert_eq!(roundtrip_timestamp.seconds, expected_dt.timestamp()); + // assert_eq!( + // roundtrip_timestamp.nanos, + // expected_dt.timestamp_subsec_nanos() as i32 + // ); + // } + // } } } From ea0210947e05cc3c82dbee85985824ccfcf7d1fc Mon Sep 17 00:00:00 2001 From: Lucas Alves de Lima Date: Sat, 26 Jul 2025 16:27:30 -0300 Subject: [PATCH 4/6] feat(prost-types,timestamp): add consts MIN and MAX --- prost-types/src/timestamp.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/prost-types/src/timestamp.rs b/prost-types/src/timestamp.rs index 61846a1b5..4485ad059 100644 --- a/prost-types/src/timestamp.rs +++ b/prost-types/src/timestamp.rs @@ -113,6 +113,16 @@ 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 { From 2e1544629bb65f07562e27f8fd031b5af52e3b24 Mon Sep 17 00:00:00 2001 From: Lucas Alves de Lima Date: Sat, 26 Jul 2025 16:31:20 -0300 Subject: [PATCH 5/6] test(prost-types,timestamp): add more comments and update unitary and kany tests --- prost-types/src/timestamp.rs | 473 +++++++++++++---------------------- 1 file changed, 167 insertions(+), 306 deletions(-) diff --git a/prost-types/src/timestamp.rs b/prost-types/src/timestamp.rs index 4485ad059..22dab551a 100644 --- a/prost-types/src/timestamp.rs +++ b/prost-types/src/timestamp.rs @@ -166,8 +166,8 @@ impl Add for Timestamp { Self { seconds, nanos: match seconds { - i64::MAX => NANOS_PER_SECOND, - i64::MIN => 0, + i64::MAX => Self::MAX.nanos, + i64::MIN => Self::MIN.nanos, _ => nanos, }, } @@ -187,20 +187,19 @@ impl Sub for Timestamp { } } -//This can be more nice, if impl overflow controlled such use i128 macro_rules! impl_div_for_integer { ($($t:ty),*) => { $( impl Div<$t> for Timestamp { type Output = Duration; - fn div(self, rhs: $t) -> Self::Output { - let total_nanos = self.seconds as i128 * NANOS_PER_SECOND as i128 + self.nanos as i128; + fn div(self, denominator: $t) -> Self::Output { + let mut total_nanos = self.seconds as i128 * NANOS_PER_SECOND as i128 + self.nanos as i128; - let result_nanos = total_nanos / (rhs as i128); + total_nanos /= denominator as i128; - let mut seconds = (result_nanos / NANOS_PER_SECOND as i128) as i64; - let mut nanos = (result_nanos % NANOS_PER_SECOND as i128) as i32; + 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; @@ -222,11 +221,16 @@ macro_rules! impl_div_for_float { impl Div<$t> for Timestamp { type Output = Duration; - fn div(self, rhs: $t) -> Self::Output { - let total_seconds_float = (self.seconds as f64 + self.nanos as f64 / NANOS_PER_SECOND as f64) / rhs as f64; + 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.0 && total_seconds_float != seconds as f64 { + if total_seconds_float < 0. && total_seconds_float != seconds as f64 { seconds -= 1; } @@ -362,6 +366,8 @@ mod tests_ops { 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() { @@ -374,7 +380,7 @@ mod tests_ops { nanos: 0, }; - assert_eq!((ts + dur).seconds, i64::MAX); + assert_eq!((ts + dur), Timestamp::MAX); } #[test] @@ -572,7 +578,7 @@ mod tests_ops { } #[cfg(kani)] -mod kani_verification_ops { +mod proofs_ops { use super::*; #[kani::proof] @@ -598,7 +604,7 @@ mod kani_verification_ops { let result = ts + dur; - assert!(result.nanos >= 0 && result.nanos < NANOS_PER_SECOND); + assert!((Timestamp::MIN.nanos..=Timestamp::MAX.nanos).contains(&result.nanos)); } #[kani::proof] @@ -624,7 +630,7 @@ mod kani_verification_ops { let result = ts - dur; - assert!(result.nanos >= 0 && result.nanos < NANOS_PER_SECOND); + assert!((Timestamp::MIN.nanos..=Timestamp::MAX.nanos).contains(&result.nanos)); } #[kani::proof] @@ -642,7 +648,7 @@ mod kani_verification_ops { let result = ts / divisor; - assert!(result.nanos >= 0 && result.nanos < NANOS_PER_SECOND); + assert!((Timestamp::MIN.nanos..=Timestamp::MAX.nanos).contains(&result.nanos)); } #[kani::proof] @@ -656,7 +662,7 @@ mod kani_verification_ops { let result = ts / divisor; - assert!(result.nanos >= 0 && result.nanos <= NANOS_PER_SECOND); + assert!((Timestamp::MIN.nanos..=Timestamp::MAX.nanos).contains(&result.nanos)); } } @@ -670,7 +676,7 @@ impl Name for Timestamp { } #[cfg(feature = "chrono")] -mod chrono { +mod timestamp_chrono { use super::*; use ::chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; @@ -721,115 +727,6 @@ mod chrono { DateTime::try_from(timestamp).map(|date_time| date_time.date_naive()) } } - - impl From for Timestamp { - fn from(naive_time: NaiveTime) -> Self { - NaiveDate::default().and_time(naive_time).and_utc().into() - } - } - - impl TryFrom for NaiveTime { - type Error = TimestampError; - - fn try_from(timestamp: Timestamp) -> Result { - DateTime::try_from(timestamp).map(|date_time| date_time.time()) - } - } - - #[cfg(test)] - mod tests { - use ::chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; - - #[test] - fn test_datetime_roundtrip() { - let original_dt = Utc::now(); - 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(2023, 10, 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_naivetime_roundtrip() { - let original_nt = NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap(); - // From converts to a timestamp on the default date (1970-01-01). - let timestamp: Timestamp = original_nt.into(); - let converted_nt: NaiveTime = timestamp.try_into().unwrap(); - assert_eq!(original_nt, converted_nt); - } - - #[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 = "std")] @@ -866,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. @@ -874,15 +772,16 @@ pub enum TimestampError { /// Indicates an error when constructing a timestamp due to invalid date or time data. InvalidDateTime, - #[cfg(feature = "chrono")] /// 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, @@ -895,7 +794,6 @@ impl fmt::Display for TimestampError { TimestampError::InvalidDateTime => { write!(f, "invalid date or time") } - #[cfg(feature = "chrono")] TimestampError::OutOfChronoDateTimeRanges(timestamp) => { write!( @@ -971,186 +869,58 @@ mod proofs { } #[cfg(feature = "chrono")] - mod kani_chrono { + mod p_chrono { use super::*; - use ::chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc}; + 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); - // } - // } - - // #[kani::proof] - // fn check_timestamp_roundtrip_via_naive_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_time) = NaiveTime::try_from(timestamp) { - // assert_eq!(Timestamp::from(naive_time), timestamp); - // } - // } - - // #[kani::proof] - // fn verify_from_datetime_utc() { - // let date_time: DateTime = - // DateTime::from_timestamp(kani::any(), kani::any()).unwrap_or_default(); - // let timestamp = Timestamp::from(date_time); - // assert_eq!(timestamp.seconds, date_time.timestamp()); - // assert_eq!(timestamp.nanos, date_time.timestamp_subsec_nanos() as i32); - // } - - // #[kani::proof] - // fn verify_from_naive_datetime() { - // let naive_dt: NaiveDateTime = kani::any(); - // let timestamp = Timestamp::from(naive_dt); - // let expected_dt_utc = naive_dt.and_utc(); - // assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); - // assert_eq!( - // timestamp.nanos, - // expected_dt_utc.timestamp_subsec_nanos() as i32 - // ); - // } - // - // #[kani::proof] - // fn verify_from_naive_date() { - // let naive_date: NaiveDate = kani::any(); - // let timestamp = Timestamp::from(naive_date); - // let naive_dt = naive_date.and_time(NaiveTime::default()); - // let expected_dt_utc = naive_dt.and_utc(); - // assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); - // assert_eq!( - // timestamp.nanos, - // expected_dt_utc.timestamp_subsec_nanos() as i32 - // ); - // } - // - // #[kani::proof] - // fn verify_from_naive_time() { - // let naive_time: NaiveTime = kani::any(); - // let timestamp = Timestamp::from(naive_time); - // let naive_dt = NaiveDate::default().and_time(naive_time); - // let expected_dt_utc = naive_dt.and_utc(); - // assert_eq!(timestamp.seconds, expected_dt_utc.timestamp()); - // assert_eq!( - // timestamp.nanos, - // expected_dt_utc.timestamp_subsec_nanos() as i32 - // ); - // } - // - // #[kani::proof] - // fn verify_roundtrip_from_timestamp_to_datetime() { - // let timestamp: Timestamp = kani::any(); - // // Precondition: The timestamp must be valid according to its spec. - // kani::assume((0..1_000_000_000).contains(×tamp.nanos)); - // - // if let Ok(dt_utc) = ::chrono::DateTime::::try_from(timestamp.clone()) { - // // If conversion succeeds, the reverse must also succeed and be identical. - // let roundtrip_timestamp = Timestamp::from(dt_utc); - // assert_eq!(timestamp, roundtrip_timestamp); - // } - // } - // - // #[kani::proof] - // fn verify_roundtrip_from_timestamp_to_naive_datetime() { - // let timestamp: Timestamp = kani::any(); - // kani::assume((0..1_000_000_000).contains(×tamp.nanos)); - // - // if let Ok(naive_dt) = ::chrono::NaiveDateTime::try_from(timestamp.clone()) { - // let roundtrip_timestamp = Timestamp::from(naive_dt); - // assert_eq!(timestamp, roundtrip_timestamp); - // } - // } - // - // #[kani::proof] - // fn verify_roundtrip_from_timestamp_to_naive_date() { - // let timestamp: Timestamp = kani::any(); - // kani::assume((0..1_000_000_000).contains(×tamp.nanos)); - // - // if let Ok(naive_date) = ::chrono::NaiveDate::try_from(timestamp.clone()) { - // let roundtrip_timestamp = Timestamp::from(naive_date); - // - // // The original timestamp, when converted, should match the round-tripped date. - // let original_dt = ::chrono::DateTime::::try_from(timestamp).unwrap(); - // assert_eq!(original_dt.date_naive(), naive_date); - // - // // The round-tripped timestamp should correspond to midnight of that day. - // let expected_dt = naive_date.and_time(NaiveTime::default()).and_utc(); - // assert_eq!(roundtrip_timestamp.seconds, expected_dt.timestamp()); - // assert_eq!(roundtrip_timestamp.nanos, 0); - // } - // } - // - // #[kani::proof] - // fn verify_roundtrip_from_timestamp_to_naive_time() { - // let timestamp: Timestamp = kani::any(); - // kani::assume((0..1_000_000_000).contains(×tamp.nanos)); - // - // if let Ok(naive_time) = ::chrono::NaiveTime::try_from(timestamp.clone()) { - // let roundtrip_timestamp = Timestamp::from(naive_time); - // - // // The original timestamp's time part should match the converted naive_time. - // let original_dt = ::chrono::DateTime::::try_from(timestamp).unwrap(); - // assert_eq!(original_dt.time(), naive_time); - // - // // The round-tripped timestamp should correspond to the naive_time on the epoch date. - // let expected_dt = NaiveDate::default().and_time(naive_time).and_utc(); - // assert_eq!(roundtrip_timestamp.seconds, expected_dt.timestamp()); - // assert_eq!( - // roundtrip_timestamp.nanos, - // expected_dt.timestamp_subsec_nanos() as i32 - // ); - // } - // } + #[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); + } + } } } @@ -1318,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() { From df04822a51ee488146cfd284216343eadd6e3c36 Mon Sep 17 00:00:00 2001 From: Lucas Alves de Lima Date: Sat, 26 Jul 2025 17:48:41 -0300 Subject: [PATCH 6/6] chore(nix,readme): - Add support for MSRV tokio with `nix develop .#rust_minimum_version`; - Update readme, with this nix command. --- README.md | 18 +++++++++++++++--- flake.lock | 15 ++++++++++++++- flake.nix | 13 +++++++------ 3 files changed, 36 insertions(+), 10 deletions(-) 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 0419426cb..c887fdd5e 100644 --- a/flake.lock +++ b/flake.lock @@ -73,7 +73,8 @@ "inputs": { "fenix": "fenix", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_2", + "rust_manifest": "rust_manifest" } }, "rust-analyzer-src": { @@ -93,6 +94,18 @@ "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": { "locked": { "lastModified": 1681028828, diff --git a/flake.nix b/flake.nix index 333d9402f..269631286 100644 --- a/flake.nix +++ b/flake.nix @@ -5,6 +5,10 @@ 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 = @@ -13,6 +17,7 @@ nixpkgs, flake-utils, fenix, + rust_manifest, }: flake-utils.lib.eachDefaultSystem ( system: @@ -30,7 +35,7 @@ { devShells.default = let - rustpkgs = fenix.packages.${system}.stable.defaultToolchain; + rustpkgs = fenix.packages.${system}.stable.completeToolchain; in pkgs.mkShell { packages = [ @@ -39,11 +44,7 @@ }; devShells."rust_minimum_version" = let - rust_manifest = { - url = "https://static.rust-lang.org/dist/2023-08-03/channel-rust-1.71.1.toml"; - flake = false; - }; - rustpkgs = (fenix.packages.${system}.fromManifestFile rust_manifest).defaultToolchain; + rustpkgs = (fenix.packages.${system}.fromManifestFile rust_manifest).completeToolchain; in pkgs.mkShell { packages = [