use std::collections::HashMap;
use std::path::PathBuf;
fn fixture_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/testdata/hocon/self-ref-lookback")
}
fn expected_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/testdata/expected/self-ref-lookback")
}
fn fixture_path(stem: &str) -> PathBuf {
fixture_dir().join(format!("{}.conf", stem))
}
fn error_sidecar_path(stem: &str) -> PathBuf {
expected_dir().join(format!("{}.error", stem))
}
fn expected_json_path(stem: &str) -> PathBuf {
expected_dir().join(format!("{}-expected.json", stem))
}
fn normalize(v: &serde_json::Value) -> serde_json::Value {
match v {
serde_json::Value::Object(map) => {
let mut m = serde_json::Map::new();
for (k, val) in map {
m.insert(k.clone(), normalize(val));
}
serde_json::Value::Object(m)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.iter().map(normalize).collect())
}
serde_json::Value::Number(n) => {
let f = n.as_f64().unwrap_or(0.0);
serde_json::json!(f)
}
other => other.clone(),
}
}
fn hocon_to_json(v: &hocon::HoconValue) -> serde_json::Value {
match v {
hocon::HoconValue::Object(map) => {
let mut m = serde_json::Map::new();
for (k, val) in map {
m.insert(k.clone(), hocon_to_json(val));
}
serde_json::Value::Object(m)
}
hocon::HoconValue::Array(arr) => {
serde_json::Value::Array(arr.iter().map(hocon_to_json).collect())
}
hocon::HoconValue::Scalar(sv) => match sv.value_type {
hocon::ScalarType::Null => serde_json::Value::Null,
hocon::ScalarType::Boolean => serde_json::Value::Bool(sv.raw == "true"),
hocon::ScalarType::Number => {
if !sv.raw.contains('.') && !sv.raw.contains('e') && !sv.raw.contains('E') {
if let Ok(n) = sv.raw.parse::<i64>() {
return serde_json::json!(n);
}
}
if let Ok(f) = sv.raw.parse::<f64>() {
return serde_json::json!(f);
}
serde_json::Value::String(sv.raw.clone())
}
hocon::ScalarType::String => serde_json::Value::String(sv.raw.clone()),
_ => serde_json::Value::String(sv.raw.clone()),
},
_ => panic!("hocon_to_json: unknown HoconValue variant: {:?}", v),
}
}
fn key_to_lookup_path(key: &str) -> String {
if key.is_empty()
|| key.contains('.')
|| key.contains('"')
|| key.contains('\\')
|| key.contains(' ')
|| key.contains('\t')
{
let escaped = key.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{}\"", escaped)
} else {
key.to_string()
}
}
fn config_to_json(config: &hocon::Config) -> serde_json::Value {
let mut m = serde_json::Map::new();
for key in config.keys() {
let path = key_to_lookup_path(key);
if let Some(val) = config.get(&path) {
m.insert(key.to_string(), hocon_to_json(val));
}
}
normalize(&serde_json::Value::Object(m))
}
fn run_fixture(stem: &str) {
let fp = fixture_path(stem);
let ep = error_sidecar_path(stem);
let jp = expected_json_path(stem);
assert!(
fp.exists(),
"fixture missing: {} — run `make testdata` to sync fixtures from xx.hocon",
fp.display()
);
let has_error = ep.exists();
let has_json = jp.exists();
assert!(
has_error || has_json,
"self-ref-lookback/{stem}.conf has no expected sidecar (.error or -expected.json).\n\
Run `make testdata` first to fetch expected sidecars from xx.hocon."
);
let env: HashMap<String, String> = HashMap::new();
let result = hocon::parse_file_with_env(&fp, &env);
if has_error {
assert!(
result.is_err(),
"self-ref-lookback {}: expected parse/resolve error but got Ok (fixture: {})",
stem,
fp.display()
);
} else {
let cfg = result.unwrap_or_else(|e| {
panic!(
"self-ref-lookback {}: unexpected error {:?} (fixture: {})",
stem,
e,
fp.display()
)
});
let got = config_to_json(&cfg);
let json_src = std::fs::read_to_string(&jp)
.unwrap_or_else(|e| panic!("failed to read expected JSON {}: {}", jp.display(), e));
let expected: serde_json::Value = serde_json::from_str(&json_src)
.unwrap_or_else(|e| panic!("invalid JSON in {}: {}", jp.display(), e));
let expected = normalize(&expected);
assert_eq!(
got,
expected,
"self-ref-lookback {}: output mismatch\n got: {}\n expected: {}",
stem,
serde_json::to_string_pretty(&got).unwrap(),
serde_json::to_string_pretty(&expected).unwrap(),
);
}
}
#[test]
fn sr01_optional_no_prior() {
run_fixture("sr01-optional-no-prior");
}
#[test]
fn sr02_optional_no_prior_leading() {
run_fixture("sr02-optional-no-prior-leading");
}
#[test]
fn sr03_optional_no_prior_both_sides() {
run_fixture("sr03-optional-no-prior-both-sides");
}
#[test]
fn sr04_optional_with_prior() {
run_fixture("sr04-optional-with-prior");
}
#[test]
fn sr05_required_no_prior() {
run_fixture("sr05-required-no-prior");
}
#[test]
fn sr06_required_with_prior() {
run_fixture("sr06-required-with-prior");
}
#[test]
fn sr07_array_optional_no_prior() {
run_fixture("sr07-array-optional-no-prior");
}
#[test]
fn sr08_array_optional_with_prior() {
run_fixture("sr08-array-optional-with-prior");
}
#[test]
fn sr09_nested_no_prior() {
run_fixture("sr09-nested-no-prior");
}
#[test]
fn sr10_nested_with_prior() {
run_fixture("sr10-nested-with-prior");
}
#[test]
fn sr11_mutual_ref_forward() {
run_fixture("sr11-mutual-ref-forward");
}
#[test]
fn s13a_13_nested_self_ref_object_literal_form() {
let cfg = hocon::parse_with_env(
"foo {\n a = \"x\"\n a = ${?foo.a}bar\n}",
&std::collections::HashMap::new(),
)
.expect("parse failed");
assert_eq!(cfg.get_string("foo.a").unwrap(), "xbar");
}
#[test]
fn sr12_nested_external_ref_no_prior() {
run_fixture("sr12-nested-external-ref-no-prior");
}
#[test]
fn sr13_nested_external_ref_with_prior() {
run_fixture("sr13-nested-external-ref-with-prior");
}
#[test]
fn sr14_cache_prior_external() {
run_fixture("sr14-cache-prior-external");
}
#[test]
fn sr15_double_self_ref() {
run_fixture("sr15-double-self-ref");
}
#[test]
fn sr16_external_before_self_ref() {
run_fixture("sr16-external-before-self-ref");
}
#[test]
fn sr17_pure_optional_concat_no_prior() {
let env = std::collections::HashMap::new();
let result = hocon::parse_with_env("a = ${?a}foo${?b}", &env);
assert!(
result.is_ok(),
"expected Ok but got error: {:?}",
result.err()
);
assert_eq!(
result.unwrap().get_string("a").unwrap(),
"foo",
"a = ${{?a}}foo${{?b}} no prior should resolve to \"foo\""
);
}
#[test]
fn sr18_required_external_no_def_errors() {
let env = std::collections::HashMap::new();
let result = hocon::parse_with_env("a = ${?a}foo${b}", &env);
assert!(
result.is_err(),
"expected resolve error for required missing ${{b}}, got Ok"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.contains("could not resolve substitution") || err_msg.contains("b"),
"error message should mention unresolved substitution b, got: {err_msg}"
);
}
#[test]
fn sr19_required_self_ref_mixed_no_prior() {
let env = std::collections::HashMap::new();
let result = hocon::parse_with_env("a = ${?a}foo${a}", &env);
assert!(
result.is_err(),
"expected resolve error for required self-ref ${{a}} with no prior, got Ok"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.contains("self-referential") || err_msg.contains("no prior"),
"error message should mention self-referential substitution, got: {err_msg}"
);
}
#[test]
fn sr21_strict_subset_obj_overwrite_keeps_live_self_ref() {
let env = std::collections::HashMap::new();
let input = "o.a = \"x\"\no.b = 0\no.a = ${?o.a}bar\no = {b = 1}";
let cfg = hocon::parse_with_env(input, &env)
.unwrap_or_else(|e| panic!("sr21: unexpected parse error: {:?}", e));
assert_eq!(
cfg.get_string("o.a").unwrap(),
"xbar",
"sr21: o.a should be \"xbar\" (Concat resolves via inner leaf prior o.a=\"x\"); \
got wrong value — folded outer prior incorrectly preempted leaf prior"
);
assert_eq!(
cfg.get_string("o.b").unwrap(),
"1",
"sr21: o.b should be \"1\" (deep merge of new value)"
);
}
#[test]
fn sr20_obj_overwrite_same_keys_with_self_ref() {
let env = std::collections::HashMap::new();
let input = "o.a = \"x\"\no.prev = 0\no.a = ${?o.a}bar\no = {a = 1, prev = ${?o}}";
let cfg = hocon::parse_with_env(input, &env)
.unwrap_or_else(|e| panic!("sr20: unexpected parse error: {:?}", e));
assert_eq!(
cfg.get_string("o.prev.a").unwrap(),
"xbar",
"sr20: o.prev.a should be \"xbar\" (prior o.a = ${{?o.a}}bar resolved to \"xbar\"); \
got wrong value — same-key Obj-Obj fold regression"
);
assert_eq!(
cfg.get_string("o.a").unwrap(),
"1",
"sr20: o.a should be \"1\" (last assignment wins)"
);
}