From 753be8627238f06c4095eb13af2ec77580182710 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 14 Jan 2023 17:15:48 +0100 Subject: [PATCH] added first tests --- CHANGELOG.md | 12 +++ src/time/cds.rs | 225 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 203 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d61eea4..23e98d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). # [unreleased] +## Added + +- CDS timestamp: Added constructor function to create the time provider + from `chrono::DateTime` and a generic UNIX timestamp (`i64` seconds + and subsecond milliseconds). + +## Fixed + +- CDS timestamp: The conversion function from the current time were buggy + when specifying picoseconds precision, which could lead to overflow + multiplications and incorrect precision fields. + # [v0.4.0] 10.01.2023 ## Fixed diff --git a/src/time/cds.rs b/src/time/cds.rs index 92434eb..7f8b7a7 100644 --- a/src/time/cds.rs +++ b/src/time/cds.rs @@ -163,7 +163,7 @@ impl ConversionFromUnix { fn new(unix_seconds: i64, subsec_millis: u32) -> Result { let (unix_days, secs_of_day) = calc_unix_days_and_secs_of_day(unix_seconds); let ccsds_days = unix_to_ccsds_days(unix_days); - if ccsds_days < 0 { + if ccsds_days == 0 && (secs_of_day > 0 || subsec_millis > 0) || ccsds_days < 0 { let millis = if unix_seconds < 0 { unix_seconds * 1000 - subsec_millis as i64 } else { @@ -222,37 +222,36 @@ impl CdsConverter for ConversionFromDatetime { #[inline] fn calc_unix_days_and_secs_of_day(unix_seconds: i64) -> (i64, u32) { let mut secs_of_day = unix_seconds % SECONDS_PER_DAY as i64; - let unix_days = if secs_of_day > 0 { - (unix_seconds - secs_of_day) / SECONDS_PER_DAY as i64 - } else { - (unix_seconds + secs_of_day) / SECONDS_PER_DAY as i64 - }; + let mut unix_days = (unix_seconds - secs_of_day) / SECONDS_PER_DAY as i64; + // Imagine the CCSDS epoch time minus 5 seconds: We now have the last day in the year + // 1969 (-1 unix days) shortly before midnight (SECONDS_PER_DAY - 5). if secs_of_day < 0 { - secs_of_day = -secs_of_day + unix_days -= 1; + secs_of_day = SECONDS_PER_DAY as i64 + secs_of_day } (unix_days, secs_of_day as u32) } impl ConversionFromDatetime { - fn new(dt: DateTime) -> Result { + fn new(dt: &DateTime) -> Result { Self::new_generic(dt, None) } - fn new_with_submillis_us_prec(dt: DateTime) -> Result { + fn new_with_submillis_us_prec(dt: &DateTime) -> Result { Self::new_generic(dt, Some(SubmillisPrecision::Microseconds(0))) } - fn new_with_submillis_ps_prec(dt: DateTime) -> Result { + fn new_with_submillis_ps_prec(dt: &DateTime) -> Result { Self::new_generic(dt, Some(SubmillisPrecision::Picoseconds(0))) } fn new_generic( - dt: DateTime, + dt: &DateTime, mut prec: Option, ) -> Result { // The CDS timestamp does not support timestamps before the CCSDS epoch. if dt.year() < 1958 { - return Err(TimestampError::DateBeforeCcsdsEpoch(dt)); + return Err(TimestampError::DateBeforeCcsdsEpoch(*dt)); } // The contained values in the conversion should be all positive now let unix_conversion = @@ -265,8 +264,8 @@ impl ConversionFromDatetime { )); } SubmillisPrecision::Picoseconds(_) => { - prec = Some(SubmillisPrecision::Microseconds( - (dt.timestamp_subsec_nanos() * 1000) as u16, + prec = Some(SubmillisPrecision::Picoseconds( + (dt.timestamp_subsec_nanos() * 1000), )); } _ => (), @@ -314,8 +313,8 @@ impl ConversionFromNow { )); } SubmillisPrecision::Picoseconds(_) => { - prec = Some(SubmillisPrecision::Microseconds( - (now.subsec_nanos() * 1000) as u16, + prec = Some(SubmillisPrecision::Picoseconds( + (now.subsec_nanos() * 1000), )); } _ => (), @@ -507,31 +506,31 @@ impl TimeProvider { Ok(provider) } - pub fn from_dt_generic( - dt: DateTime, + fn from_dt_generic( + dt: &DateTime, days_len: LengthOfDaySegment, ) -> Result { let conv_from_dt = ConversionFromDatetime::new(dt)?; Self::generic_from_conversion(days_len, conv_from_dt) } - pub fn from_dt_generic_us_prec( - dt: DateTime, + fn from_dt_generic_us_prec( + dt: &DateTime, days_len: LengthOfDaySegment, ) -> Result { let conv_from_dt = ConversionFromDatetime::new_with_submillis_us_prec(dt)?; Self::generic_from_conversion(days_len, conv_from_dt) } - pub fn from_dt_generic_ps_prec( - dt: DateTime, + fn from_dt_generic_ps_prec( + dt: &DateTime, days_len: LengthOfDaySegment, ) -> Result { let conv_from_dt = ConversionFromDatetime::new_with_submillis_ps_prec(dt)?; Self::generic_from_conversion(days_len, conv_from_dt) } - pub fn from_unix_generic( + fn from_unix_generic( unix_seconds: i64, subsec_millis: u32, days_len: LengthOfDaySegment, @@ -654,7 +653,7 @@ impl TimeProvider { /// This function will return [TimestampError::DateBeforeCcsdsEpoch] or /// [TimestampError::CdsError] if the time is before the CCSDS epoch (01-01-1958 00:00:00) or /// the CCSDS days value exceeds the allowed bit width (24 bits). - pub fn from_unix_seconds_u24_days( + pub fn from_unix_secs_with_u24_days( unix_seconds: i64, subsec_millis: u32, ) -> Result { @@ -668,17 +667,17 @@ impl TimeProvider { /// This function will return [TimestampError::DateBeforeCcsdsEpoch] or /// [TimestampError::CdsError] if the time is before the CCSDS epoch (01-01-1958 00:00:00) or /// the CCSDS days value exceeds the allowed bit width (24 bits). - pub fn from_dt_with_u24_days(dt: DateTime) -> Result { + pub fn from_dt_with_u24_days(dt: &DateTime) -> Result { Self::from_dt_generic(dt, LengthOfDaySegment::Long24Bits) } /// Like [Self::from_dt_with_u24_days] but with microsecond sub-millisecond precision. - pub fn from_dt_with_u24_days_us_prec(dt: DateTime) -> Result { + pub fn from_dt_with_u24_days_us_prec(dt: &DateTime) -> Result { Self::from_dt_generic_us_prec(dt, LengthOfDaySegment::Long24Bits) } /// Like [Self::from_dt_with_u24_days] but with picoseconds sub-millisecond precision. - pub fn from_dt_with_u24_days_ps_prec(dt: DateTime) -> Result { + pub fn from_dt_with_u24_days_ps_prec(dt: &DateTime) -> Result { Self::from_dt_generic_ps_prec(dt, LengthOfDaySegment::Long24Bits) } @@ -733,7 +732,14 @@ impl TimeProvider { Self::generic_new(LengthOfDaySegment::Short16Bits, ccsds_days, ms_of_day).unwrap() } - pub fn from_unix_seconds_with_u16_days( + /// Create a provider from a generic UNIX timestamp (seconds since 01-01-1970 00:00:00). + /// + /// ## Errors + /// + /// This function will return [TimestampError::DateBeforeCcsdsEpoch] or + /// [TimestampError::CdsError] if the time is before the CCSDS epoch (01-01-1958 00:00:00) or + /// the CCSDS days value exceeds the allowed bit width (24 bits). + pub fn from_unix_secs_with_u16_days( unix_seconds: i64, subsec_millis: u32, ) -> Result { @@ -745,17 +751,17 @@ impl TimeProvider { /// This function will return a [TimestampError::DateBeforeCcsdsEpoch] or a /// [TimestampError::CdsError] if the time is before the CCSDS epoch (01-01-1958 00:00:00) or /// the CCSDS days value exceeds the allowed bit width (16 bits). - pub fn from_dt_with_u16_days(dt: DateTime) -> Result { + pub fn from_dt_with_u16_days(dt: &DateTime) -> Result { Self::from_dt_generic(dt, LengthOfDaySegment::Short16Bits) } /// Like [Self::from_dt_with_u16_days] but with microsecond sub-millisecond precision. - pub fn from_dt_with_u16_days_us_precision(dt: DateTime) -> Result { + pub fn from_dt_with_u16_days_us_precision(dt: &DateTime) -> Result { Self::from_dt_generic_us_prec(dt, LengthOfDaySegment::Short16Bits) } /// Like [Self::from_dt_with_u16_days] but with picoseconds sub-millisecond precision. - pub fn from_dt_with_u16_days_ps_precision(dt: DateTime) -> Result { + pub fn from_dt_with_u16_days_ps_precision(dt: &DateTime) -> Result { Self::from_dt_generic_ps_prec(dt, LengthOfDaySegment::Short16Bits) } @@ -892,7 +898,7 @@ impl TryFrom> for TimeProvider { type Error = TimestampError; fn try_from(dt: DateTime) -> Result { - let conversion = ConversionFromDatetime::new(dt)?; + let conversion = ConversionFromDatetime::new(&dt)?; Self::generic_from_conversion(LengthOfDaySegment::Short16Bits, conversion) } } @@ -901,7 +907,7 @@ impl TryFrom> for TimeProvider { type Error = TimestampError; fn try_from(dt: DateTime) -> Result { - let conversion = ConversionFromDatetime::new(dt)?; + let conversion = ConversionFromDatetime::new(&dt)?; Self::generic_from_conversion(LengthOfDaySegment::Long24Bits, conversion) } } @@ -1000,7 +1006,7 @@ mod tests { use super::*; use crate::time::TimestampError::{ByteConversionError, InvalidTimeCode}; use crate::ByteConversionError::{FromSliceTooSmall, ToSliceTooSmall}; - use chrono::{Datelike, Timelike}; + use chrono::{Datelike, NaiveDate, Timelike}; #[cfg(feature = "serde")] use postcard::{from_bytes, to_allocvec}; use std::format; @@ -1317,6 +1323,157 @@ mod tests { } } + #[test] + fn test_creation_from_dt() { + let subsec_millis = 250; + let naivedatetime_utc = NaiveDate::from_ymd_opt(2023, 01, 14) + .unwrap() + .and_hms_milli_opt(16, 49, 30, subsec_millis) + .unwrap(); + let datetime_utc = DateTime::::from_utc(naivedatetime_utc, Utc); + let time_provider = TimeProvider::from_dt_with_u16_days(&datetime_utc).unwrap(); + // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 + // Leap years need to be accounted for as well. + assert_eq!(time_provider.ccsds_days, 23754); + assert_eq!(time_provider.ms_of_day, 30 * 1000 + 49 * 60 * 1000 + 16 * 60 * 60 * 1000 + subsec_millis); + assert_eq!(time_provider.date_time().unwrap(), datetime_utc); + let time_provider_2: TimeProvider = datetime_utc.try_into().expect("conversion failed"); + // Test the TryInto trait impl + assert_eq!(time_provider, time_provider_2); + } + + #[test] + fn test_creation_from_dt_us_prec() { + // 250 ms + 500 us + let subsec_millis = 250; + let subsec_micros = subsec_millis * 1000 + 500; + let naivedatetime_utc = NaiveDate::from_ymd_opt(2023, 01, 14) + .unwrap() + .and_hms_micro_opt(16, 49, 30, subsec_micros) + .unwrap(); + let datetime_utc = DateTime::::from_utc(naivedatetime_utc, Utc); + let time_provider = TimeProvider::from_dt_with_u16_days_us_precision(&datetime_utc).unwrap(); + // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 + // Leap years need to be accounted for as well. + assert_eq!(time_provider.ccsds_days, 23754); + assert_eq!(time_provider.ms_of_day, 30 * 1000 + 49 * 60 * 1000 + 16 * 60 * 60 * 1000 + subsec_millis); + assert!(time_provider.submillis_precision.is_some()); + match time_provider.submillis_precision.unwrap() { + SubmillisPrecision::Microseconds(us) => { + assert_eq!(us, 500); + } + _=> panic!("unexpected precision field") + } + assert_eq!(time_provider.date_time().unwrap(), datetime_utc); + } + + #[test] + fn test_creation_from_dt_ps_prec() { + // 250 ms + 500 us + let subsec_millis = 250; + let subsec_nanos = subsec_millis * 1000 * 1000 + 500 * 1000; + let naivedatetime_utc = NaiveDate::from_ymd_opt(2023, 01, 14) + .unwrap() + .and_hms_nano_opt(16, 49, 30, subsec_nanos) + .unwrap(); + let datetime_utc = DateTime::::from_utc(naivedatetime_utc, Utc); + let time_provider = TimeProvider::from_dt_with_u16_days_ps_precision(&datetime_utc).unwrap(); + // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 + // Leap years need to be accounted for as well. + assert_eq!(time_provider.ccsds_days, 23754); + assert_eq!(time_provider.ms_of_day, 30 * 1000 + 49 * 60 * 1000 + 16 * 60 * 60 * 1000 + subsec_millis); + assert!(time_provider.submillis_precision.is_some()); + match time_provider.submillis_precision.unwrap() { + SubmillisPrecision::Picoseconds(ps) => { + assert_eq!(ps, subsec_nanos * 1000); + } + _=> panic!("unexpected precision field") + } + assert_eq!(time_provider.date_time().unwrap(), datetime_utc); + } + + #[test] + fn test_creation_from_unix_stamp_0() { + let unix_secs = 0; + let subsec_millis = 0; + let time_provider = TimeProvider::from_unix_secs_with_u16_days(unix_secs, subsec_millis) + .expect("creating provider from unix stamp failed"); + assert_eq!(time_provider.ccsds_days, -DAYS_CCSDS_TO_UNIX as u16) + } + + #[test] + fn test_creation_from_unix_stamp_1() { + let subsec_millis = 250; + let naivedatetime_utc = NaiveDate::from_ymd_opt(2023, 01, 14) + .unwrap() + .and_hms_milli_opt(16, 49, 30, subsec_millis) + .unwrap(); + let datetime_utc = DateTime::::from_utc(naivedatetime_utc, Utc); + let time_provider = TimeProvider::from_unix_secs_with_u16_days(datetime_utc.timestamp(), subsec_millis) + .expect("creating provider from unix stamp failed"); + // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 + // Leap years need to be accounted for as well. + assert_eq!(time_provider.ccsds_days, 23754); + assert_eq!(time_provider.ms_of_day, 30 * 1000 + 49 * 60 * 1000 + 16 * 60 * 60 * 1000 + subsec_millis); + let dt_back = time_provider.date_time().unwrap(); + assert_eq!(datetime_utc, dt_back); + } + + #[test] + fn test_creation_0_ccsds_days() { + let unix_secs = DAYS_CCSDS_TO_UNIX as i64 * SECONDS_PER_DAY as i64; + let subsec_millis = 0; + let time_provider = TimeProvider::from_unix_secs_with_u16_days(unix_secs, subsec_millis) + .expect("creating provider from unix stamp failed"); + assert_eq!(time_provider.ccsds_days, 0) + } + + #[test] + fn test_invalid_creation_from_unix_stamp_days_too_large() { + let invalid_unix_secs: i64 = (u16::MAX as i64 + 1) * SECONDS_PER_DAY as i64; + let subsec_millis = 0; + match TimeProvider::from_unix_secs_with_u16_days(invalid_unix_secs as i64, subsec_millis) { + Ok(_) => { + panic!("creation should not succeed") + } + Err(e) => { + if let TimestampError::CdsError(CdsError::InvalidCcsdsDays(days)) = e { + assert_eq!( + days, + unix_to_ccsds_days(invalid_unix_secs / SECONDS_PER_DAY as i64) + ); + } else { + panic!("unexpected error {}", e) + } + } + } + } + + #[test] + fn test_invalid_creation_from_unix_stamp_before_ccsds_epoch() { + // This is a unix stamp before the CCSDS epoch (01-01-1958 00:00:00), this should be + // precisely 31-12-1957 23:59:55 + let unix_secs = DAYS_CCSDS_TO_UNIX * SECONDS_PER_DAY as i32 - 5; + let subsec_millis = 0; + match TimeProvider::from_unix_secs_with_u16_days(unix_secs as i64, subsec_millis) { + Ok(_) => { + panic!("creation should not succeed") + } + Err(e) => { + if let TimestampError::DateBeforeCcsdsEpoch(dt) = e { + assert_eq!(dt.year(), 1957); + assert_eq!(dt.month(), 12); + assert_eq!(dt.day(), 31); + assert_eq!(dt.hour(), 23); + assert_eq!(dt.minute(), 59); + assert_eq!(dt.second(), 55); + } else { + panic!("unexpected error {}", e) + } + } + } + } + #[test] #[cfg(feature = "serde")] fn test_serialization() {