use noyalib::{
BudgetBreach, DuplicateKeyPolicy, Error, MergeKeyPolicy, ParserConfig, Value, from_str,
from_str_with_config,
};
#[test]
fn from_str_value_refuses_distinct_typed_key_collision() {
for src in [
"1: a\n\"1\": b\n",
"true: yes\n\"true\": no\n",
"~: a\n\"null\": b\n",
] {
let err = from_str::<Value>(src)
.expect_err("distinct-typed collision must error on the Value fast path");
assert!(
matches!(err, Error::KeyCollision(_)),
"expected KeyCollision for {src:?}, got {err:?}"
);
}
}
#[test]
fn from_str_value_accepts_genuine_duplicate_keys() {
let v: Value = from_str("1: a\n1: b\n").unwrap();
let m = v.as_mapping().unwrap();
assert_eq!(m.len(), 1);
assert_eq!(m.get("1"), Some(&Value::String("b".into())));
}
#[test]
fn from_str_value_accepts_lone_scalar_key() {
assert!(from_str::<Value>("8080: service\n").is_ok());
assert!(from_str::<Value>("true: on\n").is_ok());
assert!(from_str::<Value>("a: 1\nb: 2\n").is_ok());
}
fn small_limits() -> ParserConfig {
let mut cfg = ParserConfig::default();
cfg.max_sequence_length = 4;
cfg.max_mapping_keys = 4;
cfg.max_merge_keys = 2;
cfg
}
#[test]
fn from_str_value_enforces_max_sequence_length() {
let cfg = small_limits();
let src = "[1, 2, 3, 4, 5]\n"; let err = from_str_with_config::<Value>(src, &cfg)
.expect_err("sequence-length budget must fire on the Value fast path");
let msg = format!("{err}");
assert!(
msg.contains("sequence length limit"),
"expected sequence-length budget error, got {msg:?}"
);
}
#[test]
fn from_str_value_enforces_max_mapping_keys() {
let cfg = small_limits();
let src = "a: 1\nb: 2\nc: 3\nd: 4\ne: 5\n"; let err = from_str_with_config::<Value>(src, &cfg)
.expect_err("mapping-key budget must fire on the Value fast path");
let msg = format!("{err}");
assert!(
msg.contains("mapping key limit"),
"expected mapping-key budget error, got {msg:?}"
);
}
#[test]
fn from_str_value_enforces_max_merge_keys() {
let cfg = small_limits();
let src = "\
a: &a
x: 1
b: &b
y: 2
c: &c
z: 3
one:
<<: *a
<<: *b
<<: *c
";
let err = from_str_with_config::<Value>(src, &cfg)
.expect_err("max-merge-keys budget must fire on the Value fast path");
assert!(
matches!(err, Error::Budget(BudgetBreach::MaxMergeKeys { .. })),
"expected MaxMergeKeys, got {err:?}"
);
}
#[test]
fn from_str_value_merge_key_policy_error_refused_on_value_path() {
let mut cfg = ParserConfig::default();
cfg.merge_key_policy = MergeKeyPolicy::Error;
let src = "base: &b\n x: 1\nservice:\n <<: *b\n";
let err = from_str_with_config::<Value>(src, &cfg)
.expect_err("MergeKeyPolicy::Error must reject `<<`");
let msg = format!("{err}");
assert!(
msg.contains("merge key") || msg.contains("<<"),
"unexpected message: {msg:?}"
);
}
#[test]
fn merge_keys_do_not_trip_collision_on_value_path() {
let src = "\
defaults: &d
timeout: 30
service:
<<: *d
name: web
";
let v: Value = from_str(src).unwrap();
let svc = v.as_mapping().unwrap().get("service").unwrap();
let svc = svc.as_mapping().unwrap();
assert_eq!(svc.get("timeout"), Some(&Value::Number(30i64.into())));
assert_eq!(svc.get("name"), Some(&Value::String("web".into())));
}
#[test]
fn duplicate_policy_first_still_wins_on_value_path() {
let mut cfg = ParserConfig::default();
cfg.duplicate_key_policy = DuplicateKeyPolicy::First;
let v: Value = from_str_with_config("k: one\nk: two\n", &cfg).unwrap();
let m = v.as_mapping().unwrap();
assert_eq!(m.get("k"), Some(&Value::String("one".into())));
}
#[test]
fn value_fast_path_enforces_max_documents() {
let cfg = ParserConfig::new().max_documents(1);
let src = "a: 1\n---\nb: 2\n---\nc: 3\n";
let err = from_str_with_config::<Value>(src, &cfg)
.expect_err("max_documents must be enforced on the Value fast path");
assert!(
matches!(
err,
Error::Budget(BudgetBreach::MaxDocuments { limit: 1, .. })
),
"expected MaxDocuments budget error, got {err:?}"
);
}
#[test]
fn value_fast_path_enforces_alias_anchor_ratio() {
let cfg = ParserConfig::new().alias_anchor_ratio(Some(2.0));
let src = "a: &x 1\nb: [*x, *x, *x, *x, *x, *x, *x, *x, *x, *x]\n";
let err = from_str_with_config::<Value>(src, &cfg)
.expect_err("alias_anchor_ratio must be enforced on the Value fast path");
assert!(
matches!(err, Error::Budget(BudgetBreach::AliasAnchorRatio { .. })),
"got {err:?}"
);
}
#[test]
fn value_fast_path_enforces_max_events() {
let cfg = ParserConfig::new().max_events(3);
let src = "a: 1\nb: 2\nc: 3\nd: 4\n"; let err = from_str_with_config::<Value>(src, &cfg)
.expect_err("max_events must be enforced on the Value fast path");
assert!(
matches!(err, Error::Budget(BudgetBreach::MaxEvents { .. })),
"got {err:?}"
);
}
#[test]
fn value_fast_path_enforces_max_total_scalar_bytes() {
let cfg = ParserConfig::new().max_total_scalar_bytes(4);
let src = "key: aaaaaaaaaa\n"; let err = from_str_with_config::<Value>(src, &cfg)
.expect_err("max_total_scalar_bytes must be enforced on the Value fast path");
assert!(
matches!(err, Error::Budget(BudgetBreach::MaxTotalScalarBytes { .. })),
"got {err:?}"
);
}