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 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("Exact build_tree should succeed")
}
fn records_from(items: Vec<Value>, score_path: Option<&str>) -> Vec<Record> {
Record::from_items(items, score_path)
}
mod single_axis {
use super::*;
#[test]
fn groups_by_kind() {
let items = vec![
json!({"kind": "bug"}),
json!({"kind": "bug"}),
json!({"kind": "feat"}),
json!({"kind": "docs"}),
];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
assert_eq!(clusters.len(), 3);
assert_eq!(clusters[0].label, "bug");
assert_eq!(clusters[0].total, 2);
assert_eq!(clusters[1].label, "docs");
assert_eq!(clusters[1].total, 1);
assert_eq!(clusters[2].label, "feat");
assert_eq!(clusters[2].total, 1);
}
#[test]
fn single_value_single_cluster() {
let items = vec![
json!({"kind": "bug"}),
json!({"kind": "bug"}),
json!({"kind": "bug"}),
json!({"kind": "bug"}),
];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
assert_eq!(clusters.len(), 1);
assert_eq!(clusters[0].label, "bug");
assert_eq!(clusters[0].total, 4);
}
#[test]
fn cluster_id_extends_root() {
let items = vec![json!({"kind": "bug"}), json!({"kind": "bug"})];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
let expected_id = ClusterId::parse_canonical("kind:bug").expect("canonical id parse");
assert_eq!(clusters[0].id, expected_id);
}
#[test]
fn cluster_label_is_value_string() {
let items = vec![json!({"kind": "bug"})];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
assert_eq!(clusters[0].label, "bug");
}
#[test]
fn cluster_axis_field_propagates() {
let items = vec![json!({"kind": "bug"})];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
assert_eq!(clusters[0].axis, "kind");
}
#[test]
fn cluster_value_is_json_string() {
let items = vec![json!({"kind": "bug"})];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
assert_eq!(clusters[0].value, json!("bug"));
}
}
mod scores {
use super::*;
#[test]
fn score_min_max_across_records() {
let items = vec![
json!({"kind": "bug", "score": 0.4}),
json!({"kind": "bug", "score": 0.9}),
json!({"kind": "bug", "score": 0.6}),
];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, Some(".score")));
assert_eq!(clusters.len(), 1);
assert_eq!(clusters[0].score_min, Some(0.4));
assert_eq!(clusters[0].score_max, Some(0.9));
}
#[test]
fn score_none_when_no_records_have_scores() {
let items = vec![json!({"kind": "bug"}), json!({"kind": "bug"})];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
assert_eq!(clusters.len(), 1);
assert_eq!(clusters[0].score_min, None);
assert_eq!(clusters[0].score_max, None);
}
#[test]
fn score_min_max_skip_record_with_no_score() {
let items = vec![
json!({"kind": "bug", "score": 0.4}),
json!({"kind": "bug", "score": "n/a"}),
json!({"kind": "bug", "score": 0.9}),
];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, Some(".score")));
assert_eq!(clusters.len(), 1);
assert_eq!(clusters[0].score_min, Some(0.4));
assert_eq!(clusters[0].score_max, Some(0.9));
assert_eq!(clusters[0].total, 3);
}
}
mod ordering {
use super::*;
#[test]
fn count_desc_then_label_asc() {
let items = vec![
json!({"kind": "a"}),
json!({"kind": "a"}),
json!({"kind": "a"}),
json!({"kind": "b"}),
json!({"kind": "b"}),
json!({"kind": "c"}),
json!({"kind": "d"}),
];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
let labels: Vec<&str> = clusters.iter().map(|c| c.label.as_str()).collect();
let totals: Vec<u64> = clusters.iter().map(|c| c.total).collect();
assert_eq!(labels, vec!["a", "b", "c", "d"]);
assert_eq!(totals, vec![3, 2, 1, 1]);
}
#[test]
fn tied_counts_alphabetic() {
let items = vec![
json!({"kind": "zebra"}),
json!({"kind": "alpha"}),
json!({"kind": "middle"}),
];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
let labels: Vec<&str> = clusters.iter().map(|c| c.label.as_str()).collect();
assert_eq!(labels, vec!["alpha", "middle", "zebra"]);
}
}
mod recursion_root_only {
use super::*;
#[test]
fn clusters_array_is_empty() {
let items = vec![
json!({"kind": "bug"}),
json!({"kind": "feat"}),
json!({"kind": "feat"}),
];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
assert!(!clusters.is_empty(), "must produce at least one cluster");
for c in &clusters {
assert!(
c.clusters.is_empty(),
"leaf clusters must have empty `clusters` array, got {} children",
c.clusters.len(),
);
}
}
}
mod ignored_records {
use super::*;
fn cluster_with_label<'a>(clusters: &'a [Cluster], label: &str) -> Option<&'a Cluster> {
clusters.iter().find(|c| c.label == label)
}
#[test]
fn non_string_keys_dropped() {
let items = vec![
json!({"kind": "bug"}),
json!({"kind": 5}),
json!({"kind": 5.5}),
json!({"kind": [1, 2]}),
json!({"kind": {}}),
json!({"kind": Value::Null}),
json!({"kind": true}),
];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
let labels: std::collections::HashSet<&str> =
clusters.iter().map(|c| c.label.as_str()).collect();
assert!(
labels.contains("bug"),
"string `bug` must be kept; got {labels:?}"
);
assert!(
labels.contains("5"),
"integer `5` must coerce to \"5\"; got {labels:?}"
);
assert!(
labels.contains("null"),
"null must coerce to \"null\"; got {labels:?}"
);
assert!(
labels.contains("true"),
"true must coerce to \"true\"; got {labels:?}"
);
assert!(
!labels.iter().any(|l| l.contains("5.5")),
"fractional 5.5 must be dropped; got {labels:?}",
);
for label in ["bug", "5", "null", "true"] {
let c = cluster_with_label(&clusters, label).expect("kept cluster present");
assert_eq!(c.total, 1, "kept cluster `{label}` should have one record");
}
assert_eq!(
clusters.len(),
4,
"expected exactly 4 kept clusters (bug, 5, null, true), got {}: {labels:?}",
clusters.len(),
);
}
#[test]
fn missing_path_dropped() {
let items = vec![
json!({"name": "x"}),
json!({"kind": "feat"}),
json!({"kind": "feat"}),
];
let plan = AxisPlan::leaf(exact("kind"));
let clusters = run(plan, records_from(items, None));
assert_eq!(clusters.len(), 1, "only `feat` should survive");
assert_eq!(clusters[0].label, "feat");
assert_eq!(clusters[0].total, 2);
}
}