use std::fs;
use std::path::PathBuf;
fn testdata_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/testdata/hocon")
}
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 as f64);
}
}
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()),
},
_ => unreachable!("unknown HoconValue variant"),
}
}
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 parse_and_compare(conf_path: &std::path::Path, json_path: &std::path::Path) {
let config = hocon::parse_file(conf_path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", conf_path.display(), e));
let got = config_to_json(&config);
let expected_str = fs::read_to_string(json_path)
.unwrap_or_else(|e| panic!("read {}: {}", json_path.display(), e));
let expected: serde_json::Value = serde_json::from_str(&expected_str)
.unwrap_or_else(|e| panic!("parse {}: {}", json_path.display(), e));
let expected = normalize(&expected);
assert_eq!(
got,
expected,
"mismatch for {}\ngot:\n{}\nwant:\n{}",
conf_path.display(),
serde_json::to_string_pretty(&got).unwrap(),
serde_json::to_string_pretty(&expected).unwrap()
);
}
#[test]
fn lightbend_equiv01() {
let dir = testdata_dir().join("equiv01");
let json_path = dir.join("original.json");
for entry in fs::read_dir(&dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
let ext = path.extension().and_then(|e| e.to_str());
let name = path.file_name().unwrap().to_str().unwrap();
if ext == Some("conf") || (ext == Some("json") && name != "original.json") {
parse_and_compare(&path, &json_path);
}
}
}
#[test]
fn lightbend_equiv02() {
let dir = testdata_dir().join("equiv02");
let json_path = dir.join("original.json");
for entry in fs::read_dir(&dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("conf") {
parse_and_compare(&path, &json_path);
}
}
}
#[test]
fn lightbend_equiv03() {
let dir = testdata_dir().join("equiv03");
let json_path = dir.join("original.json");
let conf_path = dir.join("includes.conf");
parse_and_compare(&conf_path, &json_path);
}
#[test]
fn lightbend_equiv04() {
let dir = testdata_dir().join("equiv04");
let json_path = dir.join("original.json");
for entry in fs::read_dir(&dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("conf") {
parse_and_compare(&path, &json_path);
}
}
}
#[test]
fn lightbend_equiv05() {
let dir = testdata_dir().join("equiv05");
let json_path = dir.join("original.json");
for entry in fs::read_dir(&dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("conf") {
parse_and_compare(&path, &json_path);
}
}
}
#[test]
fn lightbend_test01() {
let path = testdata_dir().join("test01.conf");
let config = hocon::parse_file(&path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", path.display(), e));
assert_eq!(config.get_i64("ints.fortyTwo").unwrap(), 42);
assert_eq!(config.get_i64("ints.fortyTwoAgain").unwrap(), 42);
assert_eq!(config.get_string("strings.abcd").unwrap(), "abcd");
assert_eq!(config.get_string("strings.abcdAgain").unwrap(), "abcd");
assert!(config.get_bool("booleans.true").unwrap());
assert!(!config.get_bool("booleans.false").unwrap());
}
#[test]
fn lightbend_test02_empty_keys_and_quoted_paths() {
let path = testdata_dir().join("test02.conf");
let config = hocon::parse_file(&path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", path.display(), e));
assert_eq!(config.get_i64("a.b.c").unwrap(), 57);
assert_eq!(config.get_i64("57_a").unwrap(), 57);
assert_eq!(config.get_i64("57_b").unwrap(), 57);
assert_eq!(config.get_i64("42_a").unwrap(), 42);
assert_eq!(config.get_i64("42_b").unwrap(), 42);
assert_eq!(config.get_i64("103_a").unwrap(), 103);
assert_eq!(config.get_i64("a-c").unwrap(), 259);
assert_eq!(config.get_i64("a_c").unwrap(), 260);
}
#[test]
fn lightbend_test03_includes_with_substitution_fallback() {
let path = testdata_dir().join("test03.conf");
let config = hocon::parse_file(&path).unwrap();
assert_eq!(config.get_i64("test01.booleans").unwrap(), 42);
assert_eq!(
config.get_string("b").unwrap(),
"This is in the including file",
"root b = ${{bar}} uses parent's bar (root-level include — no relativization)"
);
assert_eq!(
config.get_string("subtree.b").unwrap(),
"This is in the including file",
"subtree.b = ${{bar}} relativized to ${{subtree.bar}} then fallback strips prefix to root ${{bar}} (S14c.2 / rs.hocon#44)"
);
}
#[test]
fn lightbend_test04_akka_reference_config() {
let path = testdata_dir().join("test04.conf");
let config = hocon::parse_file(&path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", path.display(), e));
assert_eq!(config.get_string("akka.version").unwrap(), "2.0-SNAPSHOT");
assert!(config.has("akka.actor"));
}
#[test]
fn lightbend_test05_play_application_config() {
let path = testdata_dir().join("test05.conf");
let config = hocon::parse_file(&path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", path.display(), e));
assert_eq!(
config.get_string("application.name").unwrap(),
"Yet Another Blog Engine"
);
assert_eq!(config.get_string("db").unwrap(), "mem");
}
#[test]
fn lightbend_test06_delayed_merge() {
let path = testdata_dir().join("test06.conf");
let config = hocon::parse_file(&path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", path.display(), e));
assert_eq!(config.get_i64("x").unwrap(), 2);
assert_eq!(config.get_i64("y.foo").unwrap(), 10);
assert_eq!(config.get_string("y.hello").unwrap(), "world");
}
#[test]
fn lightbend_test07_classpath_include() {
let path = testdata_dir().join("test07.conf");
let _config = hocon::parse_file(&path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", path.display(), e));
}
#[test]
fn lightbend_test08_classpath_include_absolute() {
let path = testdata_dir().join("test08.conf");
let _config = hocon::parse_file(&path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", path.display(), e));
}
#[test]
fn lightbend_test09_delayed_merge_object() {
let path = testdata_dir().join("test09.conf");
let config = hocon::parse_file(&path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", path.display(), e));
assert_eq!(config.get_i64("a.c").unwrap(), 3);
assert_eq!(config.get_i64("x.q").unwrap(), 10);
}
#[test]
fn lightbend_test10_nested_include() {
let path = testdata_dir().join("test10.conf");
let config = hocon::parse_file(&path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", path.display(), e));
assert_eq!(config.get_i64("foo.y").unwrap(), 5);
assert_eq!(config.get_i64("foo.a.c").unwrap(), 3);
assert_eq!(config.get_i64("foo.a.q").unwrap(), 10);
assert_eq!(config.get_i64("bar.nested.y").unwrap(), 5);
assert_eq!(config.get_i64("bar.nested.b").unwrap(), 5);
assert_eq!(config.get_i64("bar.nested.a.c").unwrap(), 3);
assert_eq!(config.get_i64("bar.nested.a.q").unwrap(), 10);
}
#[test]
fn lightbend_test11_numeric_string_keys() {
let path = testdata_dir().join("test11.conf");
let config = hocon::parse_file(&path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", path.display(), e));
assert_eq!(config.get_string("10").unwrap(), "42");
assert_eq!(config.get_string("-10").unwrap(), "-42");
assert_eq!(config.get_string("foo-bar").unwrap(), "bar-baz");
assert_eq!(config.get_string("---").unwrap(), "------");
assert_eq!(config.get_string("a-").unwrap(), "b-");
}
#[test]
fn lightbend_test12_long_numeric_keys() {
let path = testdata_dir().join("test12.conf");
let config = hocon::parse_file(&path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", path.display(), e));
assert_eq!(config.get_string("10").unwrap(), "42");
assert_eq!(config.get_i64("sth").unwrap(), 42);
assert_eq!(
config
.get_string("12345678901234567891234567890123456789")
.unwrap(),
"42"
);
}
#[test]
fn lightbend_test13_substitution_override() {
let ref_path = testdata_dir().join("test13-reference-with-substitutions.conf");
let app_path = testdata_dir().join("test13-application-override-substitutions.conf");
let ref_config = hocon::parse_file(&ref_path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", ref_path.display(), e));
let app_config = hocon::parse_file(&app_path)
.unwrap_or_else(|e| panic!("ParseFile({}) failed: {}", app_path.display(), e));
assert_eq!(ref_config.get_string("a").unwrap(), "b");
assert_eq!(ref_config.get_string("b").unwrap(), "b");
assert_eq!(app_config.get_string("b").unwrap(), "overridden");
let merged = app_config.with_fallback(&ref_config);
assert_eq!(merged.get_string("b").unwrap(), "overridden");
let ref_text = fs::read_to_string(&ref_path).unwrap();
let app_text = fs::read_to_string(&app_path).unwrap();
let combined = format!("{}\n{}", ref_text, app_text);
let resolved = hocon::parse(&combined).unwrap();
assert_eq!(resolved.get_string("a").unwrap(), "overridden");
}
#[test]
fn lightbend_test13_bad_substitution() {
let path = testdata_dir().join("test13-reference-bad-substitutions.conf");
let result = hocon::parse_file(&path);
assert!(
result.is_err(),
"Expected error for unresolved substitution in test13-reference-bad-substitutions.conf"
);
}
#[test]
fn lightbend_suite_expected_json() {
let testdata = testdata_dir();
let expected_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/testdata/expected");
if !expected_dir.exists() {
eprintln!(
"Skipping: expected JSON fixtures not found at {}. Run `make testdata` first.",
expected_dir.display()
);
return;
}
let skip: std::collections::HashSet<&str> = [
"test01-expected.json", "test10-expected.json", "test03-expected.json",
]
.into_iter()
.collect();
let mut tested = 0;
for entry in fs::read_dir(&expected_dir).unwrap() {
let entry = entry.unwrap();
let name = entry.file_name().to_string_lossy().to_string();
if !name.ends_with("-expected.json") {
continue;
}
if skip.contains(name.as_str()) {
eprintln!("SKIP (known failure): {}", name);
continue;
}
let conf_name = name.replace("-expected.json", ".conf");
let conf_path = testdata.join(&conf_name);
let expected_path = expected_dir.join(&name);
if !conf_path.exists() {
eprintln!("SKIP (conf not found): {}", conf_name);
continue;
}
eprintln!("TEST: {} vs {}", conf_name, name);
parse_and_compare(&conf_path, &expected_path);
tested += 1;
}
assert!(
tested > 0,
"No expected JSON tests were run. Check tests/testdata/expected/"
);
}
#[test]
fn lightbend_suite_expected_errors() {
let testdata = testdata_dir();
let expected_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/testdata/expected");
if !expected_dir.exists() {
eprintln!(
"Skipping: expected JSON fixtures not found at {}. Run `make testdata` first.",
expected_dir.display()
);
return;
}
let mut tested = 0;
for entry in fs::read_dir(&expected_dir).unwrap() {
let entry = entry.unwrap();
let name = entry.file_name().to_string_lossy().to_string();
if !name.ends_with("-expected-error.json") {
continue;
}
let conf_name = name.replace("-expected-error.json", ".conf");
let conf_path = testdata.join(&conf_name);
if !conf_path.exists() {
eprintln!("SKIP (conf not found): {}", conf_name);
continue;
}
eprintln!("TEST (expect error): {}", conf_name);
let result = hocon::parse_file(&conf_path);
assert!(
result.is_err(),
"Expected error for {} but got success",
conf_path.display()
);
tested += 1;
}
assert!(
tested > 0,
"No expected error tests were run. Check tests/testdata/expected/"
);
}
#[test]
fn subst_tokenize_success_suite() {
let testdata = testdata_dir();
let subst_dir = testdata.join("subst-tokenize");
let expected_dir =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/testdata/expected/subst-tokenize");
if !subst_dir.exists() || !expected_dir.exists() {
eprintln!(
"Skipping subst_tokenize_success_suite: fixtures not found. \
Run `make testdata` to populate. subst_dir={}, expected_dir={}",
subst_dir.display(),
expected_dir.display()
);
return;
}
let mut tested = 0;
for entry in fs::read_dir(&expected_dir).unwrap() {
let entry = entry.unwrap();
let name = entry.file_name().to_string_lossy().to_string();
if !name.ends_with("-expected.json") || name.contains("-expected-error") {
continue;
}
let conf_name = name.replace("-expected.json", ".conf");
let conf_path = subst_dir.join(&conf_name);
let expected_path = expected_dir.join(&name);
if !conf_path.exists() {
panic!("conf not found: {}", conf_path.display());
}
eprintln!("TEST: subst-tokenize/{} vs {}", conf_name, name);
parse_and_compare(&conf_path, &expected_path);
tested += 1;
}
assert!(
tested >= 20,
"expected >= 20 success fixtures, got {}",
tested
);
}
#[test]
fn subst_tokenize_error_suite() {
let subst_dir = testdata_dir().join("subst-tokenize");
let expected_dir =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/testdata/expected/subst-tokenize");
if !subst_dir.exists() || !expected_dir.exists() {
eprintln!(
"Skipping subst_tokenize_error_suite: fixtures not found. \
Run `make testdata` to populate."
);
return;
}
let mut tested = 0;
for entry in fs::read_dir(&expected_dir).unwrap() {
let entry = entry.unwrap();
let name = entry.file_name().to_string_lossy().to_string();
if !name.ends_with("-expected-error.json") {
continue;
}
let conf_name = name.replace("-expected-error.json", ".conf");
let conf_path = subst_dir.join(&conf_name);
if !conf_path.exists() {
panic!("conf not found: {}", conf_path.display());
}
eprintln!("TEST: subst-tokenize/{} (expect error)", conf_name);
let result = hocon::parse_file(&conf_path);
assert!(
result.is_err(),
"expected error for {}, got Ok",
conf_path.display()
);
tested += 1;
}
assert!(
tested >= 11,
"expected >= 11 error fixtures, got {}",
tested
);
}