#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::sync::Arc;
use arrow::array::Array;
use chronix::prelude::*;
use chronix::{Chronix, fields, tags};
fn counter_db(dir: &tempfile::TempDir, flushes: i64, per_flush: i64) -> Arc<Chronix> {
let db = Arc::new(
Chronix::open(
ChronixConfig::builder()
.data_dir(dir.path())
.build()
.unwrap(),
)
.unwrap(),
);
let key = SeriesKey::new("ctr", tags! { "host" => "h" }).unwrap();
for f in 0..flushes {
let points: Vec<Point> = (0..per_flush)
.map(|i| {
let idx = f * per_flush + i;
Point::new(
key.clone(),
fields! { "v" => idx as f64 },
idx * 1_000_000_000,
)
.unwrap()
})
.collect();
assert!(db.insert_batch(&points).unwrap().is_complete());
db.flush().unwrap();
}
db
}
async fn scalar(ctx: &datafusion::prelude::SessionContext, sql: &str) -> Option<f64> {
let b = ctx.sql(sql).await.unwrap().collect().await.unwrap();
let col = b[0]
.column(0)
.as_any()
.downcast_ref::<arrow::array::Float64Array>()
.unwrap();
(!col.is_null(0)).then(|| col.value(0))
}
#[tokio::test]
async fn rate_is_independent_of_scan_partitioning() {
let dir = tempfile::tempdir().unwrap();
let db = counter_db(&dir, 40, 500);
let ctx = chronix::sql::create_session_context(db);
let r = scalar(&ctx, "SELECT rate(v, _time) AS r FROM ctr")
.await
.expect("rate over 20000 samples must be defined");
assert!(
(r - 1.0).abs() < 1e-9,
"a 1/s counter that never resets must rate at 1.0/s, got {r}"
);
let r = scalar(
&ctx,
"SELECT rate(v, _time) AS r FROM ctr GROUP BY host ORDER BY host",
)
.await
.unwrap();
assert!(
(r - 1.0).abs() < 1e-9,
"grouped rate must also be 1.0, got {r}"
);
}
#[tokio::test]
async fn irate_is_independent_of_scan_partitioning() {
let dir = tempfile::tempdir().unwrap();
let db = counter_db(&dir, 40, 500);
let ctx = chronix::sql::create_session_context(db);
let r = scalar(&ctx, "SELECT irate(v, _time) AS r FROM ctr")
.await
.unwrap();
assert!((r - 1.0).abs() < 1e-9, "irate must be 1.0/s, got {r}");
}
#[tokio::test]
async fn a_counter_reset_is_still_detected() {
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(
Chronix::open(
ChronixConfig::builder()
.data_dir(dir.path())
.build()
.unwrap(),
)
.unwrap(),
);
let key = SeriesKey::new("ctr", tags! { "host" => "h" }).unwrap();
let values: Vec<f64> = (0..5).chain(0..5).map(f64::from).collect();
let points: Vec<Point> = values
.iter()
.enumerate()
.map(|(i, v)| {
Point::new(key.clone(), fields! { "v" => *v }, i as i64 * 1_000_000_000).unwrap()
})
.collect();
assert!(db.insert_batch(&points).unwrap().is_complete());
db.flush().unwrap();
let ctx = chronix::sql::create_session_context(db);
let r = scalar(&ctx, "SELECT rate(v, _time) AS r FROM ctr")
.await
.unwrap();
let expected = 8.0 / 9.0;
assert!(
(r - expected).abs() < 1e-9,
"a restart contributes its new value as the increase: expected {expected}, got {r}"
);
}
#[tokio::test]
async fn a_degenerate_series_has_no_rate() {
let dir = tempfile::tempdir().unwrap();
let db = counter_db(&dir, 1, 1);
let ctx = chronix::sql::create_session_context(db);
assert_eq!(
scalar(&ctx, "SELECT rate(v, _time) AS r FROM ctr").await,
None,
"one sample cannot have a rate"
);
}