use chrono::{Duration, Utc};
use proptest::prelude::*;
use tiny_counter::{EventStore, TimeUnit};
fn time_unit_strategy() -> impl Strategy<Value = TimeUnit> {
prop_oneof![
Just(TimeUnit::Seconds),
Just(TimeUnit::Minutes),
Just(TimeUnit::Hours),
Just(TimeUnit::Days),
Just(TimeUnit::Weeks),
Just(TimeUnit::Months),
Just(TimeUnit::Years),
]
}
fn create_store(bucket_count: usize, time_unit: TimeUnit) -> EventStore {
let mut builder = EventStore::builder();
builder = match time_unit {
TimeUnit::Seconds => builder.track_seconds(bucket_count),
TimeUnit::Minutes => builder.track_minutes(bucket_count),
TimeUnit::Hours => builder.track_hours(bucket_count),
TimeUnit::Days => builder.track_days(bucket_count),
TimeUnit::Weeks => builder.track_weeks(bucket_count),
TimeUnit::Months => builder.track_months(bucket_count),
TimeUnit::Years => builder.track_years(bucket_count),
TimeUnit::Ever => builder.track_days(bucket_count),
};
builder.build().unwrap()
}
fn record_events_at_times(store: &EventStore, event_id: &str, events: Vec<(u32, i64)>) {
let now = Utc::now();
for (count, seconds_ago) in events {
if count == 0 {
continue;
}
let timestamp = now - Duration::seconds(seconds_ago);
let _ = store.record_count_at(event_id, count, timestamp);
}
}
proptest! {
#[test]
fn merge_commutative(
count_a in 1u32..100,
count_b in 1u32..100,
bucket_count in 5usize..15,
time_unit in time_unit_strategy(),
) {
if matches!(time_unit, TimeUnit::Ever) {
return Ok(());
}
let store_a1 = create_store(bucket_count, time_unit);
store_a1.record_count("test", count_a);
let store_b1 = create_store(bucket_count, time_unit);
store_b1.record_count("test", count_b);
let exported_b = store_b1.export_all().unwrap();
store_a1.merge_all(exported_b).unwrap();
let sum_ab = store_a1.query("test").last_minutes(1).sum();
let store_a2 = create_store(bucket_count, time_unit);
store_a2.record_count("test", count_a);
let store_b2 = create_store(bucket_count, time_unit);
store_b2.record_count("test", count_b);
let exported_a = store_a2.export_all().unwrap();
store_b2.merge_all(exported_a).unwrap();
let sum_ba = store_b2.query("test").last_minutes(1).sum();
prop_assert_eq!(sum_ab, sum_ba);
}
#[test]
fn merge_associative(
count_a in 1u32..50,
count_b in 1u32..50,
count_c in 1u32..50,
bucket_count in 5usize..10,
time_unit in time_unit_strategy(),
) {
if matches!(time_unit, TimeUnit::Ever) {
return Ok(());
}
let store_a1 = create_store(bucket_count, time_unit);
store_a1.record_count("test", count_a);
let store_b1 = create_store(bucket_count, time_unit);
store_b1.record_count("test", count_b);
let store_c1 = create_store(bucket_count, time_unit);
store_c1.record_count("test", count_c);
let exported_b1 = store_b1.export_all().unwrap();
store_a1.merge_all(exported_b1).unwrap();
let exported_c1 = store_c1.export_all().unwrap();
store_a1.merge_all(exported_c1).unwrap();
let sum_abc = store_a1.query("test").last_minutes(1).sum();
let store_a2 = create_store(bucket_count, time_unit);
store_a2.record_count("test", count_a);
let store_b2 = create_store(bucket_count, time_unit);
store_b2.record_count("test", count_b);
let store_c2 = create_store(bucket_count, time_unit);
store_c2.record_count("test", count_c);
let exported_c2 = store_c2.export_all().unwrap();
store_b2.merge_all(exported_c2).unwrap();
let exported_bc = store_b2.export_all().unwrap();
store_a2.merge_all(exported_bc).unwrap();
let sum_abc2 = store_a2.query("test").last_minutes(1).sum();
prop_assert_eq!(sum_abc, sum_abc2);
}
#[test]
fn merge_identity(
event_count in 1u32..100,
bucket_count in 5usize..15,
time_unit in time_unit_strategy(),
) {
if matches!(time_unit, TimeUnit::Ever) {
return Ok(());
}
let store = create_store(bucket_count, time_unit);
store.record_count("test", event_count);
let sum_before = store.query("test").last_minutes(1).sum();
let empty_store = create_store(bucket_count, time_unit);
let empty_export = empty_store.export_all().unwrap();
store.merge_all(empty_export).unwrap();
let sum_after = store.query("test").last_minutes(1).sum();
prop_assert_eq!(sum_before, sum_after);
}
}
proptest! {
#[test]
fn record_n_equals_record_count_n(count in 1u32..100) {
let store1 = EventStore::new();
for _ in 0..count {
store1.record("event");
}
let store2 = EventStore::new();
store2.record_count("event", count);
let sum1 = store1.query("event").last_days(1).sum();
let sum2 = store2.query("event").last_days(1).sum();
prop_assert_eq!(sum1, sum2);
}
#[test]
fn query_sum_bounded_by_total(
events in prop::collection::vec(1u32..50, 1..10),
bucket_count in 5usize..20,
) {
let store = EventStore::builder()
.track_minutes(bucket_count)
.build()
.unwrap();
let event_data: Vec<_> = events
.iter()
.enumerate()
.map(|(i, &count)| (count, (i as i64) * 60))
.collect();
record_events_at_times(&store, "test", event_data);
let expected: u32 = events.iter().sum();
let sum = store.query("test").last_minutes(bucket_count).sum().unwrap_or(0);
prop_assert!(sum <= expected);
}
#[test]
fn query_respects_range_bounds(
events in prop::collection::vec(1u32..20, 5..15),
) {
let bucket_count = 20;
let store = EventStore::builder()
.track_minutes(bucket_count)
.build()
.unwrap();
let event_data: Vec<_> = events
.iter()
.enumerate()
.map(|(i, &count)| (count, (i as i64) * 60))
.collect();
record_events_at_times(&store, "test", event_data);
let subset_sum = store.query("test").last_minutes(10).sum().unwrap_or(0);
let full_sum = store.query("test").last_minutes(bucket_count).sum().unwrap_or(0);
prop_assert!(subset_sum <= full_sum);
}
}
proptest! {
#[test]
fn counts_remain_non_negative(
events in prop::collection::vec(1u32..100, 1..20),
) {
let store = EventStore::new();
for &count in &events {
store.record_count("test", count);
}
let result = store.query("test").last_days(1).sum();
prop_assert!(result.is_some());
prop_assert!(result.unwrap() > 0);
}
#[test]
fn no_data_loss_during_recording(
initial_count in 1u32..100,
additional_count in 1u32..100,
) {
let store = EventStore::new();
store.record_count("test", initial_count);
let sum_before = store.query("test").last_days(1).sum().unwrap();
prop_assert_eq!(sum_before, initial_count);
store.record_count("test", additional_count);
let sum_after = store.query("test").last_days(1).sum().unwrap();
prop_assert_eq!(sum_after, initial_count + additional_count);
}
}