use face_core::{Axis, AxisPlan, Cluster, ClusterId, NullDiagnostics, Record, build_tree};
use serde_json::{Value, json};
fn axis(field: &str, strategy: Value) -> Axis {
let mut object = serde_json::Map::new();
object.insert("field".into(), Value::String(field.into()));
object.insert("auto".into(), Value::Bool(false));
if let Value::Object(map) = strategy {
for (k, v) in map {
object.insert(k, v);
}
} else {
panic!("strategy must be a JSON object");
}
serde_json::from_value(Value::Object(object)).expect("axis must deserialize")
}
fn bands(field: &str, count: u8) -> Axis {
axis(field, json!({"strategy": "bands", "count": count}))
}
fn exact(field: &str) -> Axis {
axis(field, json!({"strategy": "exact"}))
}
fn run(plan: AxisPlan, records: Vec<Record>) -> Vec<Cluster> {
let mut diag = NullDiagnostics;
build_tree(&plan, records, &mut diag).expect("Bands build_tree should succeed")
}
fn records_from(items: Vec<Value>, score_path: Option<&str>) -> Vec<Record> {
Record::from_items(items, score_path)
}
fn value_bound(cluster: &Cluster, key: &str) -> f64 {
cluster.value[key].as_f64().unwrap_or_else(|| {
panic!("cluster.value[{key:?}] should be a JSON number; got {cluster:?}")
})
}
mod basic {
use super::*;
fn dataset() -> Vec<Record> {
let items = vec![
json!({"score": 0.1}),
json!({"score": 0.2}),
json!({"score": 0.5}),
json!({"score": 0.7}),
json!({"score": 0.9}),
json!({"score": 1.0}),
];
records_from(items, None)
}
#[test]
fn partitions_records_into_bands() {
let plan = AxisPlan::leaf(bands("score", 5));
let clusters = run(plan, dataset());
assert!(
!clusters.is_empty(),
"expected at least one cluster, got none",
);
assert!(
clusters.len() <= 5,
"expected at most 5 clusters (count=5), got {}: {:?}",
clusters.len(),
clusters.iter().map(|c| c.label.clone()).collect::<Vec<_>>(),
);
let lowers: Vec<f64> = clusters.iter().map(|c| value_bound(c, "min")).collect();
let mut sorted = lowers.clone();
sorted.sort_by(|a, b| b.total_cmp(a));
assert_eq!(
lowers, sorted,
"clusters must be sorted by lower bound descending",
);
}
#[test]
fn total_equals_input_count() {
let plan = AxisPlan::leaf(bands("score", 5));
let clusters = run(plan, dataset());
let sum: u64 = clusters.iter().map(|c| c.total).sum();
assert_eq!(
sum, 6,
"sum of cluster totals must equal input record count (6); got {sum}",
);
}
#[test]
fn cluster_id_uses_label_segment() {
let plan = AxisPlan::leaf(bands("score", 5));
let clusters = run(plan, dataset());
for cluster in &clusters {
let rendered = cluster.id.to_string();
let parsed = ClusterId::parse_canonical(&rendered).unwrap_or_else(|err| {
panic!("cluster id {rendered:?} must parse via parse_canonical; err: {err:?}",)
});
assert_eq!(
parsed, cluster.id,
"cluster id must round-trip via canonical form",
);
}
}
}
mod edges {
use super::*;
#[test]
fn single_record_yields_one_cluster() {
let items = vec![json!({"score": 0.5})];
let plan = AxisPlan::leaf(bands("score", 5));
let clusters = run(plan, records_from(items, None));
assert_eq!(clusters.len(), 1, "single record → exactly one cluster");
assert_eq!(clusters[0].total, 1);
}
#[test]
fn empty_records_returns_empty() {
let plan = AxisPlan::leaf(bands("score", 5));
let clusters = run(plan, Vec::new());
assert!(
clusters.is_empty(),
"empty input must produce empty cluster vec; got {} clusters",
clusters.len(),
);
}
#[test]
fn all_records_same_value_yields_one_cluster() {
let items = vec![
json!({"score": 0.5}),
json!({"score": 0.5}),
json!({"score": 0.5}),
];
let plan = AxisPlan::leaf(bands("score", 5));
let clusters = run(plan, records_from(items, None));
assert_eq!(
clusters.len(),
1,
"all-equal values must collapse to one cluster",
);
assert_eq!(clusters[0].total, 3);
}
#[test]
fn non_numeric_records_dropped() {
let items = vec![
json!({"score": 0.5}),
json!({"score": "high"}), json!({"score": 0.7}),
json!({"score": null}), json!({}), ];
let plan = AxisPlan::leaf(bands("score", 5));
let clusters = run(plan, records_from(items, None));
let sum: u64 = clusters.iter().map(|c| c.total).sum();
assert_eq!(
sum, 2,
"only the two numeric records (0.5, 0.7) should appear in clusters; got total {sum}",
);
}
}
mod label {
use super::*;
fn dataset() -> Vec<Record> {
let items = vec![
json!({"score": 0.1}),
json!({"score": 0.5}),
json!({"score": 1.0}),
];
records_from(items, None)
}
#[test]
fn label_contains_en_dash() {
let plan = AxisPlan::leaf(bands("score", 5));
let clusters = run(plan, dataset());
let has_en_dash = clusters.iter().any(|c| c.label.contains('\u{2013}'));
assert!(
has_en_dash,
"expected at least one cluster label to contain en-dash (U+2013); got {:?}",
clusters.iter().map(|c| c.label.clone()).collect::<Vec<_>>(),
);
}
#[test]
fn label_format_is_min_dash_max() {
let plan = AxisPlan::leaf(bands("score", 5));
let clusters = run(plan, dataset());
for cluster in &clusters {
let parts: Vec<&str> = cluster.label.split('\u{2013}').collect();
assert_eq!(
parts.len(),
2,
"label {:?} must split into exactly two parts on en-dash; got {} parts",
cluster.label,
parts.len(),
);
let lo: f64 = parts[0].trim().parse().unwrap_or_else(|err| {
panic!("label lhs {:?} must parse as f64; err: {err}", parts[0])
});
let hi: f64 = parts[1].trim().parse().unwrap_or_else(|err| {
panic!("label rhs {:?} must parse as f64; err: {err}", parts[1])
});
assert!(
lo <= hi,
"label {:?} must have lo <= hi; got lo={lo}, hi={hi}",
cluster.label,
);
}
}
}
mod value_shape {
use super::*;
#[test]
fn cluster_value_has_min_max_keys() {
let items = vec![json!({"score": 0.1}), json!({"score": 1.0})];
let plan = AxisPlan::leaf(bands("score", 5));
let clusters = run(plan, records_from(items, None));
for cluster in &clusters {
assert!(
cluster.value.is_object(),
"cluster.value must be a JSON object for Bands; got {:?}",
cluster.value,
);
let min = &cluster.value["min"];
let max = &cluster.value["max"];
assert!(
min.is_number(),
"cluster.value[\"min\"] must be a JSON number; got {min:?}",
);
assert!(
max.is_number(),
"cluster.value[\"max\"] must be a JSON number; got {max:?}",
);
let lo = min.as_f64().unwrap();
let hi = max.as_f64().unwrap();
assert!(
lo <= hi,
"cluster.value range must satisfy min <= max; got min={lo}, max={hi}",
);
}
}
}
mod recursion {
use super::*;
#[test]
fn bands_within_exact() {
let items = vec![
json!({"score": 0.1, "kind": "bug"}),
json!({"score": 0.2, "kind": "bug"}),
json!({"score": 0.5, "kind": "feat"}),
json!({"score": 0.7, "kind": "bug"}),
json!({"score": 0.9, "kind": "feat"}),
json!({"score": 1.0, "kind": "feat"}),
];
let outer = bands("score", 3);
let inner = exact("kind");
let plan = AxisPlan::with(outer, AxisPlan::leaf(inner));
let clusters = run(plan, records_from(items, None));
assert!(
!clusters.is_empty(),
"outer Bands must produce at least one cluster",
);
let outer_total: u64 = clusters.iter().map(|c| c.total).sum();
assert_eq!(outer_total, 6);
for cluster in &clusters {
assert!(
!cluster.clusters.is_empty(),
"each Bands cluster must recurse into Exact children; cluster {:?} has none",
cluster.label,
);
let child_total: u64 = cluster.clusters.iter().map(|c| c.total).sum();
assert_eq!(
child_total, cluster.total,
"child totals must add up to parent total in cluster {:?}",
cluster.label,
);
for child in &cluster.clusters {
assert_eq!(child.axis, "kind");
}
}
}
}