use super::*;
use std::time::Duration;
#[test]
fn test_latency_histogram_observe() {
let hist = LatencyHistogram::new();
hist.observe(Duration::from_micros(500)); hist.observe(Duration::from_millis(3)); hist.observe(Duration::from_millis(75));
assert_eq!(hist.count(), 3);
let buckets = hist.bucket_counts();
assert_eq!(buckets[0], 1); assert_eq!(buckets[1], 1); assert_eq!(buckets[4], 1); }
#[test]
fn test_latency_histogram_avg() {
let hist = LatencyHistogram::new();
hist.observe(Duration::from_millis(10));
hist.observe(Duration::from_millis(20));
hist.observe(Duration::from_millis(30));
let avg = hist.avg_ns();
assert!((avg - 20_000_000.0).abs() < 1000.0);
}
#[test]
fn test_latency_histogram_reset() {
let hist = LatencyHistogram::new();
hist.observe(Duration::from_millis(10));
assert_eq!(hist.count(), 1);
hist.reset();
assert_eq!(hist.count(), 0);
assert_eq!(hist.sum_ns(), 0);
}
#[test]
fn test_graph_metrics_edge_insert() {
let metrics = GraphMetrics::new();
metrics.record_edge_insert();
metrics.record_edge_insert();
assert_eq!(metrics.edges_total(), 2);
assert_eq!(metrics.edge_inserts_total(), 2);
}
#[test]
fn test_graph_metrics_traversal() {
let metrics = GraphMetrics::new();
metrics.record_traversal(Duration::from_millis(50), 1000);
metrics.record_traversal(Duration::from_millis(100), 2000);
assert_eq!(metrics.traversals_total(), 2);
assert_eq!(metrics.traversal_nodes_visited(), 3000);
assert_eq!(metrics.traversal_latency.count(), 2);
}
#[test]
fn test_graph_metrics_prometheus_format() {
let metrics = GraphMetrics::new();
metrics.record_edge_inserts_batch(1, Duration::from_millis(5));
metrics.record_traversal(Duration::from_millis(10), 100);
let output = to_prometheus(&[("g", &metrics)]);
assert!(output.contains("# HELP velesdb_graph_edges_total"));
assert!(output.contains("velesdb_graph_edges_total{collection=\"g\"} 1"));
assert!(output.contains("velesdb_graph_edge_insert_duration_seconds_bucket"));
}
#[test]
fn test_graph_metrics_reset() {
let metrics = GraphMetrics::new();
metrics.record_edge_inserts_batch(1, Duration::from_millis(5));
metrics.reset();
assert_eq!(metrics.edges_total(), 0);
assert_eq!(metrics.edge_insert_latency.count(), 0);
}
#[test]
fn test_latency_histogram_empty_avg() {
let hist = LatencyHistogram::new();
assert!(hist.avg_ns().abs() < f64::EPSILON);
}
#[test]
fn test_latency_histogram_large_duration() {
let hist = LatencyHistogram::new();
hist.observe(Duration::from_secs(15));
let buckets = hist.bucket_counts();
assert_eq!(buckets[9], 1); }
#[test]
fn an_observation_on_a_bucket_bound_lands_in_that_bucket() {
let hist = LatencyHistogram::new();
hist.observe(Duration::from_millis(5));
let counts = hist.bucket_counts();
assert_eq!(
counts[1], 1,
"5 ms must fall in le=\"5\" (index 1), got {counts:?}"
);
assert_eq!(counts[2], 0, "it must not spill into le=\"10\"");
}
#[test]
fn every_bucket_bound_is_inclusive() {
for (index, bound) in BUCKET_BOUNDS_MS.into_iter().enumerate() {
let hist = LatencyHistogram::new();
hist.observe(Duration::from_millis(bound));
let counts = hist.bucket_counts();
assert_eq!(
counts[index], 1,
"{bound} ms must land in le=\"{bound}\" (index {index}), got {counts:?}"
);
}
}
#[test]
fn an_observation_above_the_last_bound_overflows() {
let hist = LatencyHistogram::new();
hist.observe(Duration::from_millis(10_001));
assert_eq!(hist.bucket_counts()[9], 1, "overflow bucket keeps its role");
}
#[test]
fn exported_bucket_labels_are_the_recorded_bounds_in_seconds() {
let metrics = GraphMetrics::new();
metrics.record_edge_inserts_batch(1, Duration::from_millis(5));
let output = to_prometheus(&[("g", &metrics)]);
for (index, bound_ms) in BUCKET_BOUNDS_MS.into_iter().enumerate() {
#[allow(clippy::cast_precision_loss)]
let seconds = bound_ms as f64 / 1000.0;
let expected = u64::from(index >= 1);
let line = format!(
"velesdb_graph_edge_insert_duration_seconds_bucket{{collection=\"g\",le=\"{seconds}\"}} {expected}"
);
assert!(
output.contains(&line),
"missing or miscounted bucket line: {line}\n--- output ---\n{output}"
);
}
assert!(
output.contains(
"velesdb_graph_edge_insert_duration_seconds_bucket{collection=\"g\",le=\"+Inf\"} 1"
),
"the overflow bucket must close the cumulative series\n--- output ---\n{output}"
);
}
#[test]
fn each_family_is_declared_once_and_every_sample_is_labelled() {
let a = GraphMetrics::new();
let b = GraphMetrics::new();
a.record_edge_insert();
b.record_edge_insert();
b.record_edge_insert();
let output = to_prometheus(&[("alpha", &a), ("beta", &b)]);
for family in COUNTER_FAMILIES.map(|(name, _, _)| name) {
assert_eq!(
output.matches(&format!("# HELP {family} ")).count(),
1,
"{family} must declare HELP exactly once\n--- output ---\n{output}"
);
assert_eq!(
output.matches(&format!("# TYPE {family} ")).count(),
1,
"{family} must declare TYPE exactly once\n--- output ---\n{output}"
);
}
assert!(output.contains("velesdb_graph_edges_total{collection=\"alpha\"} 1"));
assert!(output.contains("velesdb_graph_edges_total{collection=\"beta\"} 2"));
for line in output.lines() {
if line.starts_with('#') || line.is_empty() {
continue;
}
assert!(
line.contains("collection=\""),
"unlabelled sample would collide across collections: {line}"
);
}
}
#[test]
fn a_collection_name_can_never_need_label_escaping() {
for candidate in ["a\"b", "a\\b", "a\nb", "a\"; evil=\""] {
assert!(
crate::validation::validate_collection_name(candidate).is_err(),
"{candidate:?} would need label escaping but is accepted as a name"
);
}
for candidate in ["docs-v2", "a_b", "Graph1"] {
assert!(crate::validation::validate_collection_name(candidate).is_ok());
assert!(!candidate.contains(['"', '\\', '\n']));
}
}
#[test]
fn no_collections_exports_nothing() {
assert!(to_prometheus(&[]).is_empty());
}