use face_core::{ClusterId, ClusterIdError, ClusterIdSegment};
fn seg(axis: &str, value: &str) -> ClusterIdSegment {
ClusterIdSegment::new(axis, value)
}
fn id(pairs: &[(&str, &str)]) -> ClusterId {
ClusterId::new(pairs.iter().map(|(a, v)| seg(a, v)).collect())
}
mod parse_canonical {
use super::*;
#[test]
fn single_axis_bare() {
let parsed = ClusterId::parse_canonical("excellent").expect("bare single-axis must parse");
assert_eq!(parsed.segments(), [seg("", "excellent")]);
assert_eq!(parsed.depth(), 1);
}
#[test]
fn two_axis_bare() {
let parsed = ClusterId::parse_canonical("file:src/cli.rs,score:excellent")
.expect("two-axis bare must parse");
assert_eq!(
parsed.segments(),
[seg("file", "src/cli.rs"), seg("score", "excellent")]
);
assert_eq!(parsed.depth(), 2);
}
#[test]
fn three_axis_bare() {
let parsed = ClusterId::parse_canonical("repo:oops-rs,file:src/cli.rs,score:excellent")
.expect("three-axis bare must parse");
assert_eq!(
parsed.segments(),
[
seg("repo", "oops-rs"),
seg("file", "src/cli.rs"),
seg("score", "excellent"),
]
);
}
#[test]
fn value_with_comma() {
let parsed = ClusterId::parse_canonical(r#"file:"src/cli, alt.rs",score:excellent"#)
.expect("quoted value with comma must parse");
assert_eq!(
parsed.segments(),
[seg("file", "src/cli, alt.rs"), seg("score", "excellent"),]
);
}
#[test]
fn value_with_colon() {
let parsed =
ClusterId::parse_canonical(r#"key:"a:b""#).expect("quoted value with colon must parse");
assert_eq!(parsed.segments(), [seg("key", "a:b")]);
}
#[test]
fn value_with_embedded_quote() {
let parsed = ClusterId::parse_canonical(r#"key:"a""b""#)
.expect("quoted value with embedded quote must parse");
assert_eq!(parsed.segments(), [seg("key", "a\"b")]);
}
#[test]
fn value_with_all_specials() {
let parsed = ClusterId::parse_canonical(r#"key:"a,b:c""d""#)
.expect("quoted value with comma+colon+quote must parse");
assert_eq!(parsed.segments(), [seg("key", "a,b:c\"d")]);
}
}
mod parse_structured {
use super::*;
#[test]
fn two_axis() {
let parsed = ClusterId::parse_structured(["file=src/cli.rs", "score=excellent"])
.expect("two-axis structured must parse");
assert_eq!(
parsed.segments(),
[seg("file", "src/cli.rs"), seg("score", "excellent"),]
);
}
#[test]
fn value_with_equals() {
let parsed = ClusterId::parse_structured(["query=a=b"]).expect("value with `=` must parse");
assert_eq!(parsed.segments(), [seg("query", "a=b")]);
}
#[test]
fn value_with_comma_no_quoting_required() {
let parsed = ClusterId::parse_structured(["file=src/cli, alt.rs"])
.expect("value with comma must parse without quoting");
assert_eq!(parsed.segments(), [seg("file", "src/cli, alt.rs")]);
}
}
mod display_round_trip {
use super::*;
fn assert_round_trip(label: &str, original: ClusterId) {
let serialized = original.to_string();
let reparsed = ClusterId::parse_canonical(&serialized)
.unwrap_or_else(|e| panic!("[{label}] reparse of {serialized:?} failed: {e:?}"));
assert_eq!(
reparsed, original,
"[{label}] round-trip mismatch via {serialized:?}",
);
}
#[test]
fn bare_values() {
assert_round_trip(
"two-axis-bare",
id(&[("file", "src/cli.rs"), ("score", "excellent")]),
);
}
#[test]
fn value_with_comma() {
assert_round_trip("comma", id(&[("file", "src/cli, alt.rs")]));
}
#[test]
fn value_with_colon() {
assert_round_trip("colon", id(&[("key", "a:b")]));
}
#[test]
fn value_with_double_quote() {
assert_round_trip("dquote", id(&[("key", "a\"b")]));
}
#[test]
fn value_with_already_escaped_pair() {
assert_round_trip("escaped-pair", id(&[("key", "\"\"")]));
}
#[test]
fn value_with_all_three_specials() {
assert_round_trip("all-three", id(&[("key", "a,b:c\"d")]));
}
#[test]
fn empty_value() {
assert_round_trip("empty-value", id(&[("axis", "")]));
}
#[test]
fn multi_axis_combinations() {
assert_round_trip(
"three-axis",
id(&[
("repo", "oops-rs"),
("file", "src/cli.rs"),
("score", "excellent"),
]),
);
assert_round_trip(
"three-axis-with-specials",
id(&[("repo", "oops-rs, alt"), ("file", "a:b"), ("score", "x\"y")]),
);
}
#[test]
fn single_segment_bare_value() {
let original = ClusterId::new(vec![seg("", "excellent")]);
assert_eq!(original.to_string(), "excellent");
assert_round_trip("single-bare", original);
}
#[test]
fn single_segment_bare_with_quote_required_value() {
let original = ClusterId::new(vec![seg("", "src/cli, alt.rs")]);
assert_eq!(original.to_string(), r#""src/cli, alt.rs""#);
assert_round_trip("single-bare-quoted", original);
}
}
mod errors {
use super::*;
#[test]
fn empty_input() {
let err = ClusterId::parse_canonical("").expect_err("empty input must error");
assert_eq!(err, ClusterIdError::Empty);
}
#[test]
fn missing_separator_multi_segment_no_colons() {
let err = ClusterId::parse_canonical("abc,def")
.expect_err("comma-separated segments without `:` must error");
match err {
ClusterIdError::MissingSeparator { segment } => {
assert_eq!(segment, 0, "first segment is the offender");
}
other => panic!("expected MissingSeparator, got {other:?}"),
}
}
#[test]
fn unterminated_quote() {
let err = ClusterId::parse_canonical(r#"file:"src/cli.rs"#)
.expect_err("unterminated quote must error");
match err {
ClusterIdError::UnterminatedQuote { segment } => {
assert_eq!(segment, 0);
}
other => panic!("expected UnterminatedQuote, got {other:?}"),
}
}
#[test]
fn empty_axis() {
let err = ClusterId::parse_canonical(":value").expect_err("empty axis must error");
match err {
ClusterIdError::EmptyAxis { segment } => {
assert_eq!(segment, 0);
}
other => panic!("expected EmptyAxis, got {other:?}"),
}
}
#[test]
fn garbage_after_quote() {
let err = ClusterId::parse_canonical(r#"file:"x"y"#)
.expect_err("garbage after closing quote must error");
match err {
ClusterIdError::GarbageAfterQuote { segment, ch } => {
assert_eq!(segment, 0);
assert_eq!(ch, 'y');
}
other => panic!("expected GarbageAfterQuote, got {other:?}"),
}
}
#[test]
fn structured_missing_equals() {
let err = ClusterId::parse_structured(["filewithoutequals"])
.expect_err("structured part lacking `=` must error");
match err {
ClusterIdError::StructuredMissingEquals { ref segment } => {
assert_eq!(
segment, "filewithoutequals",
"error carries the offending part",
);
}
other => panic!("expected StructuredMissingEquals, got {other:?}"),
}
}
}
mod serde {
use super::*;
use ::serde::{Deserialize, Serialize};
#[test]
fn serialize_to_json_string() {
let cid = id(&[("file", "src/cli.rs"), ("score", "excellent")]);
let json = serde_json::to_string(&cid).expect("serialize");
let expected = format!("\"{}\"", cid);
assert_eq!(json, expected);
}
#[test]
fn deserialize_from_json_string() {
let cid = id(&[("file", "src/cli.rs"), ("score", "excellent")]);
let json = serde_json::to_string(&cid).expect("serialize");
let back: ClusterId = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, cid);
}
#[test]
fn deserialize_from_quoted_value() {
let json = r#""file:\"src/cli, alt.rs\",score:excellent""#;
let back: ClusterId = serde_json::from_str(json).expect("deserialize quoted value");
assert_eq!(
back.segments(),
[seg("file", "src/cli, alt.rs"), seg("score", "excellent"),]
);
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Wrap {
id: ClusterId,
}
#[test]
fn round_trip_inside_struct() {
let original = Wrap {
id: id(&[("file", "a,b"), ("score", "x\"y")]),
};
let json = serde_json::to_string(&original).expect("serialize wrap");
let back: Wrap = serde_json::from_str(&json).expect("deserialize wrap");
assert_eq!(back, original);
}
}
mod helpers {
use super::*;
use std::str::FromStr;
#[test]
fn root_is_root() {
let root = ClusterId::new(vec![]);
assert_eq!(root.depth(), 0);
assert!(root.is_root());
assert_eq!(root.parent(), None);
}
#[test]
fn parent_of_one_segment_is_root() {
let one = id(&[("file", "src/cli.rs")]);
let parent = one.parent().expect("one-segment id has a parent");
assert!(parent.is_root());
assert_eq!(parent.depth(), 0);
}
#[test]
fn parent_of_three_segment_strips_last() {
let three = id(&[
("repo", "oops-rs"),
("file", "src/cli.rs"),
("score", "excellent"),
]);
let parent = three.parent().expect("three-segment id has a parent");
assert_eq!(parent.depth(), 2);
assert_eq!(
parent.segments(),
[seg("repo", "oops-rs"), seg("file", "src/cli.rs")]
);
}
#[test]
fn depth_and_is_root_smoke() {
assert!(ClusterId::new(vec![]).is_root());
assert!(!id(&[("a", "b")]).is_root());
assert_eq!(id(&[("a", "b")]).depth(), 1);
assert_eq!(id(&[("a", "b"), ("c", "d")]).depth(), 2);
}
#[test]
fn from_str_matches_parse_canonical() {
let via_from_str =
ClusterId::from_str("file:src/cli.rs,score:excellent").expect("from_str ok");
let via_parse = ClusterId::parse_canonical("file:src/cli.rs,score:excellent")
.expect("parse_canonical ok");
assert_eq!(via_from_str, via_parse);
}
#[test]
fn from_str_propagates_error() {
let err = ClusterId::from_str("").expect_err("empty must error");
assert_eq!(err, ClusterIdError::Empty);
}
}