use tsoracle_core::{LOGICAL_MAX, Timestamp};
use tsoracle_proto::v1::GetTsResponse;
use crate::MAX_TIMESTAMPS_PER_RPC;
use crate::error::ClientError;
#[derive(Clone, Copy, Debug)]
pub(crate) struct TimestampRange {
physical_ms: u64,
logical_start: u32,
count: u32,
}
impl TimestampRange {
pub(crate) fn new(physical_ms: u64, logical_start: u32, count: u32) -> Self {
TimestampRange {
physical_ms,
logical_start,
count,
}
}
pub(crate) fn count(&self) -> u32 {
self.count
}
pub(crate) fn iter(&self) -> impl Iterator<Item = Timestamp> {
let physical_ms = self.physical_ms;
let logical_start = self.logical_start;
(0..self.count).map(move |i| Timestamp::pack(physical_ms, logical_start + i))
}
}
pub(crate) fn decode_get_ts_response(
resp: GetTsResponse,
requested: u32,
) -> Result<TimestampRange, ClientError> {
if resp.count != requested {
return Err(ClientError::Rpc(tonic::Status::internal(format!(
"server returned count={} for requested count={}",
resp.count, requested
))));
}
if resp.count == 0 || resp.count > MAX_TIMESTAMPS_PER_RPC {
return Err(ClientError::Rpc(tonic::Status::internal(format!(
"server returned out-of-range count={}",
resp.count
))));
}
let last_logical = resp.logical_start.checked_add(resp.count.saturating_sub(1));
match last_logical {
Some(last) if last <= LOGICAL_MAX => {}
_ => {
return Err(ClientError::Rpc(tonic::Status::internal(format!(
"server returned logical_start={} + count={} that overflows \
LOGICAL_MAX={}",
resp.logical_start, resp.count, LOGICAL_MAX
))));
}
}
Timestamp::try_pack(resp.physical_ms, resp.logical_start).map_err(|e| {
ClientError::Rpc(tonic::Status::internal(format!(
"server returned invalid timestamp fields: {e}"
)))
})?;
Ok(TimestampRange::new(
resp.physical_ms,
resp.logical_start,
resp.count,
))
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use tsoracle_core::PHYSICAL_MS_MAX;
#[test]
fn timestamp_range_iter_yields_contiguous_packed_timestamps() {
let range = TimestampRange::new(1_000, 5, 3);
assert_eq!(range.count(), 3);
let timestamps: Vec<Timestamp> = range.iter().collect();
assert_eq!(
timestamps,
vec![
Timestamp::pack(1_000, 5),
Timestamp::pack(1_000, 6),
Timestamp::pack(1_000, 7),
],
);
}
#[test]
fn rejects_out_of_range_physical_ms() {
let err = decode_get_ts_response(
GetTsResponse {
physical_ms: PHYSICAL_MS_MAX + 1,
logical_start: 0,
count: 1,
epoch_hi: 0,
epoch_lo: 0,
},
1,
)
.unwrap_err();
assert!(matches!(err, ClientError::Rpc(_)));
}
#[test]
fn rejects_zero_count_when_requested_zero() {
let err = decode_get_ts_response(
GetTsResponse {
physical_ms: 1,
logical_start: 0,
count: 0,
epoch_hi: 0,
epoch_lo: 0,
},
0,
)
.unwrap_err();
let ClientError::Rpc(status) = err else {
panic!("expected Rpc, got something else");
};
assert!(status.message().contains("out-of-range count=0"));
}
#[test]
fn rejects_oversized_count_when_requested_oversized() {
let oversized = MAX_TIMESTAMPS_PER_RPC + 1;
let err = decode_get_ts_response(
GetTsResponse {
physical_ms: 1,
logical_start: 0,
count: oversized,
epoch_hi: 0,
epoch_lo: 0,
},
oversized,
)
.unwrap_err();
let ClientError::Rpc(status) = err else {
panic!("expected Rpc, got something else");
};
assert!(status.message().contains("out-of-range"));
}
fn well_formed_response() -> impl Strategy<Value = GetTsResponse> {
(
0u64..=PHYSICAL_MS_MAX,
1u32..=MAX_TIMESTAMPS_PER_RPC,
any::<u64>(),
)
.prop_flat_map(|(physical_ms, count, epoch)| {
let max_logical_start = (LOGICAL_MAX + 1).saturating_sub(count);
(0u32..=max_logical_start).prop_map(move |logical_start| GetTsResponse {
physical_ms,
logical_start,
count,
epoch_hi: 0,
epoch_lo: epoch,
})
})
}
proptest! {
#[test]
fn rejects_count_mismatch(
response in well_formed_response(),
requested in 1u32..=MAX_TIMESTAMPS_PER_RPC,
) {
prop_assume!(response.count != requested);
prop_assert!(matches!(
decode_get_ts_response(response, requested),
Err(ClientError::Rpc(_))
));
}
#[test]
fn rejects_logical_overflow(
physical_ms in 0u64..=PHYSICAL_MS_MAX,
logical_start in 1u32..=LOGICAL_MAX,
count in 1u32..=MAX_TIMESTAMPS_PER_RPC,
) {
prop_assume!((logical_start as u64) + (count as u64) > (LOGICAL_MAX as u64) + 1);
let response =
GetTsResponse { physical_ms, logical_start, count, epoch_hi: 0, epoch_lo: 0 };
prop_assert!(matches!(
decode_get_ts_response(response, count),
Err(ClientError::Rpc(_))
));
}
#[test]
fn well_formed_response_decodes_successfully(response in well_formed_response()) {
let GetTsResponse { physical_ms, logical_start, count, .. } = response;
let range = decode_get_ts_response(response, count).unwrap();
prop_assert_eq!(range.count(), count);
let timestamps: Vec<Timestamp> = range.iter().collect();
prop_assert_eq!(timestamps.len(), count as usize);
for (index, timestamp) in timestamps.iter().enumerate() {
prop_assert_eq!(timestamp.physical_ms(), physical_ms);
prop_assert_eq!(timestamp.logical(), logical_start + index as u32);
}
for pair in timestamps.windows(2) {
prop_assert!(pair[0] < pair[1]);
}
}
}
}