#![allow(clippy::unwrap_used, clippy::expect_used)]
use chronix::prelude::*;
const CONFORMS_TO: &str = "3.14";
const IMPLEMENTED: &[&str] = &[
"rate",
"irate",
"increase",
"delta",
"idelta",
"deriv",
"predict_linear",
"resets",
"changes",
"avg_over_time",
"min_over_time",
"max_over_time",
"sum_over_time",
"count_over_time",
"quantile_over_time",
"stddev_over_time",
"stdvar_over_time",
"last_over_time",
"present_over_time",
"absent_over_time",
"mad_over_time",
"double_exponential_smoothing",
"abs",
"absent",
"ceil",
"floor",
"round",
"sgn",
"clamp",
"clamp_max",
"clamp_min",
"exp",
"ln",
"log2",
"log10",
"sqrt",
"scalar",
"vector",
"timestamp",
"time",
"pi",
"acos",
"acosh",
"asin",
"asinh",
"atan",
"atanh",
"cos",
"cosh",
"sin",
"sinh",
"tan",
"tanh",
"deg",
"rad",
"label_join",
"label_replace",
"sort",
"sort_desc",
"sort_by_label",
"sort_by_label_desc",
"histogram_quantile",
"histogram_count",
"histogram_sum",
"histogram_avg",
"histogram_fraction",
"histogram_stddev",
"histogram_stdvar",
"year",
"month",
"day_of_month",
"day_of_week",
"day_of_year",
"days_in_month",
"hour",
"minute",
];
const NOT_IMPLEMENTED: &[(&str, &str)] = &[
("histogram_quantiles", "experimental upstream"),
("limitk", "experimental upstream"),
("limit_ratio", "experimental upstream"),
("info", "experimental upstream"),
("ts_of_min_over_time", "experimental upstream"),
("ts_of_max_over_time", "experimental upstream"),
("ts_of_last_over_time", "experimental upstream"),
("ts_of_first_over_time", "experimental upstream"),
("first_over_time", "experimental upstream"),
("start_timestamp", "experimental upstream"),
("start", "experimental upstream"),
("end", "experimental upstream"),
("range", "experimental upstream"),
("step", "experimental upstream"),
("min_of", "experimental upstream"),
("max_of", "experimental upstream"),
];
fn db() -> (tempfile::TempDir, Chronix) {
let dir = tempfile::tempdir().expect("tempdir");
let db = Chronix::open_small(dir.path()).expect("open");
let key = SeriesKey::new("m", tags! { "host" => "a" }).expect("key");
for i in 0..5i64 {
let p = Point::new(
key.clone(),
fields! { "value" => i as f64 },
1_700_000_000_000_000_000 + i * 1_000_000_000,
)
.expect("point");
db.insert(&p).expect("insert");
}
(dir, db)
}
fn call(name: &str) -> String {
const RANGE: &[&str] = &[
"rate",
"irate",
"increase",
"delta",
"idelta",
"deriv",
"resets",
"changes",
"avg_over_time",
"min_over_time",
"max_over_time",
"sum_over_time",
"count_over_time",
"stddev_over_time",
"stdvar_over_time",
"last_over_time",
"present_over_time",
"absent_over_time",
"mad_over_time",
"first_over_time",
"ts_of_min_over_time",
"ts_of_max_over_time",
"ts_of_last_over_time",
"ts_of_first_over_time",
];
match name {
n if RANGE.contains(&n) => format!("{n}(m[5m])"),
"quantile_over_time" => format!("{name}(0.9, m[5m])"),
"predict_linear" => format!("{name}(m[5m], 60)"),
"double_exponential_smoothing" => format!("{name}(m[5m], 0.5, 0.5)"),
"histogram_quantile" | "histogram_fraction" => format!("{name}(0.9, m)"),
"histogram_quantiles" => format!("{name}(\"q\", 0.9, m)"),
"clamp" => format!("{name}(m, 0, 1)"),
"clamp_max" | "clamp_min" | "round" => format!("{name}(m, 1)"),
"label_join" => format!("{name}(m, \"dst\", \",\", \"host\")"),
"label_replace" => format!("{name}(m, \"dst\", \"$1\", \"host\", \"(.*)\")"),
"sort_by_label" | "sort_by_label_desc" => format!("{name}(m, \"host\")"),
"limitk" => format!("{name}(1, m)"),
"limit_ratio" => format!("{name}(0.5, m)"),
"min_of" | "max_of" => format!("{name}(1, 2)"),
"time" | "pi" | "start" | "end" | "range" | "step" => format!("{name}()"),
"scalar" | "vector" => format!("{name}(1)"),
_ => format!("{name}(m)"),
}
}
fn refused_by_name(err: &str, name: &str) -> bool {
let e = err.to_lowercase();
(e.contains("unknown function") || e.contains("unsupported")) && e.contains(name)
}
#[test]
fn every_function_this_file_calls_implemented_is_accepted() {
let (_dir, db) = db();
let at = 1_700_000_004_000_000_000;
let mut missing = Vec::new();
for name in IMPLEMENTED {
if let Err(e) = db.promql(&call(name), at) {
let msg = e.to_string();
if refused_by_name(&msg, name) {
missing.push((*name, msg));
}
}
}
assert!(
missing.is_empty(),
"these are listed as implemented and the evaluator does not know them \
— either the list is stale or a function was lost: {missing:#?}"
);
}
#[test]
fn every_function_this_file_calls_absent_is_refused_by_name() {
let (_dir, db) = db();
let at = 1_700_000_004_000_000_000;
let mut answered = Vec::new();
for (name, why) in NOT_IMPLEMENTED {
match db.promql(&call(name), at) {
Ok(_) => answered.push((*name, *why)),
Err(e) => {
let msg = e.to_string();
assert!(
refused_by_name(&msg, name),
"`{name}` is not implemented ({why}) and must be refused by \
name, so a user learns the function is missing rather than \
reading a parse error: got {msg}"
);
}
}
}
assert!(
answered.is_empty(),
"these are listed as not implemented and the evaluator answered them. \
A function that works is a function to document and move to \
IMPLEMENTED — silence here is how the surface drifts: {answered:#?}"
);
}
#[test]
fn the_surface_names_the_version_it_conforms_to() {
assert!(
CONFORMS_TO.starts_with('3'),
"chronix targets the Prometheus 3 series"
);
assert!(
!IMPLEMENTED.is_empty() && !NOT_IMPLEMENTED.is_empty(),
"both directions must be stated: a list of what works, without a list \
of what deliberately does not, is the shape that went stale"
);
let mut all: Vec<&str> = IMPLEMENTED.to_vec();
all.extend(NOT_IMPLEMENTED.iter().map(|(n, _)| *n));
let before = all.len();
all.sort_unstable();
all.dedup();
assert_eq!(
before,
all.len(),
"a function is either implemented or it is not; one cannot be in both lists"
);
}