use std::collections::HashMap;
use std::path::PathBuf;
const KNOWN_LIGHTBEND_QUIRKS: &[&str] = &["ce05-object-plus-scalar"];
fn fixture_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/testdata/hocon/concat-errors")
}
fn expected_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/testdata/expected/concat-errors")
}
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);
let has_error = ep.exists();
let has_json = jp.exists();
if !has_error && !has_json {
if KNOWN_LIGHTBEND_QUIRKS.contains(&stem) {
eprintln!(
"note: concat-errors/{}.conf is a known Lightbend quirk — skipping validation",
stem
);
return;
}
panic!(
"concat-errors/{stem}.conf has no expected sidecar (.error or -expected.json).\n\
Run `make testdata` first to fetch expected sidecars from xx.hocon.\n\
If this fixture is intentionally unsupported by Lightbend, add \"{stem}\" to \
KNOWN_LIGHTBEND_QUIRKS in tests/concat_errors_test.rs."
);
}
let env: HashMap<String, String> = HashMap::new();
let result = hocon::parse_file_with_env(&fp, &env);
if has_error {
assert!(
result.is_err(),
"concat-errors {}: expected parse/resolve error but got Ok (fixture: {})",
stem,
fp.display()
);
} else {
let cfg = result.unwrap_or_else(|e| {
panic!(
"concat-errors {}: 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,
"concat-errors {}: output mismatch\n got: {}\n expected: {}",
stem,
serde_json::to_string_pretty(&got).unwrap(),
serde_json::to_string_pretty(&expected).unwrap(),
);
}
}
#[test]
fn ce01_array_plus_object() {
run_fixture("ce01-array-plus-object");
}
#[test]
fn ce02_object_plus_array() {
run_fixture("ce02-object-plus-array");
}
#[test]
fn ce03_array_plus_scalar() {
run_fixture("ce03-array-plus-scalar");
}
#[test]
fn ce04_scalar_plus_array() {
run_fixture("ce04-scalar-plus-array");
}
#[test]
fn ce05_object_plus_scalar() {
run_fixture("ce05-object-plus-scalar");
}
#[test]
fn ce06_scalar_plus_object() {
run_fixture("ce06-scalar-plus-object");
}
#[test]
fn ce07_subst_obj_plus_array() {
run_fixture("ce07-subst-obj-plus-array");
}
#[test]
fn ce08_subst_array_plus_obj() {
run_fixture("ce08-subst-array-plus-obj");
}
#[test]
fn ce09_numeric_obj_still_works() {
run_fixture("ce09-numeric-obj-still-works");
}
#[test]
fn ce10_empty_array_plus_object() {
run_fixture("ce10-empty-array-plus-object");
}
#[test]
fn ce11_array_plus_empty_object() {
run_fixture("ce11-array-plus-empty-object");
}
#[test]
fn ce12_string_concat_resolved_array() {
run_fixture("ce12-string-concat-resolved-array");
}
#[test]
fn ce13_string_concat_resolved_object() {
run_fixture("ce13-string-concat-resolved-object");
}
#[test]
fn ce14_optional_missing_mid_concat() {
run_fixture("ce14-optional-missing-mid-concat");
}
#[test]
fn ce15_optional_missing_suppresses_pair() {
run_fixture("ce15-optional-missing-suppresses-pair");
}