#[cfg(test)]
use crate::{parse_huml, HumlNumber, HumlValue};
#[cfg(test)]
use serde_json::Value as JsonValue;
#[cfg(test)]
use std::fs;
#[cfg(test)]
#[derive(serde::Deserialize, Debug)]
struct AssertionTest {
name: String,
input: String,
error: bool,
}
#[cfg(test)]
fn huml_to_json(value: &HumlValue) -> JsonValue {
match value {
HumlValue::String(s) => JsonValue::String(s.clone()),
HumlValue::Number(n) => match n {
HumlNumber::Integer(i) => JsonValue::Number(serde_json::Number::from(*i)),
HumlNumber::Float(f) => {
if let Some(num) = serde_json::Number::from_f64(*f) {
JsonValue::Number(num)
} else {
JsonValue::Null
}
}
HumlNumber::Nan => JsonValue::String("nan".to_string()),
HumlNumber::Infinity(positive) => {
if *positive {
JsonValue::String("inf".to_string())
} else {
JsonValue::String("-inf".to_string())
}
}
},
HumlValue::Boolean(b) => JsonValue::Bool(*b),
HumlValue::Null => JsonValue::Null,
HumlValue::List(items) => JsonValue::Array(items.iter().map(huml_to_json).collect()),
HumlValue::Dict(dict) => {
let mut map = serde_json::Map::new();
for (key, value) in dict {
map.insert(key.clone(), huml_to_json(value));
}
JsonValue::Object(map)
}
}
}
#[cfg(test)]
fn normalize_json_value(value: JsonValue) -> JsonValue {
match value {
JsonValue::Number(n) => {
if let Some(f) = n.as_f64() {
if f.is_infinite() {
if f.is_sign_positive() {
JsonValue::String("inf".to_string())
} else {
JsonValue::String("-inf".to_string())
}
} else if f.is_nan() {
JsonValue::String("nan".to_string())
} else if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
JsonValue::Number(serde_json::Number::from(f as i64))
} else {
JsonValue::Number(n)
}
} else {
JsonValue::Number(n)
}
}
JsonValue::Array(arr) => {
JsonValue::Array(arr.into_iter().map(normalize_json_value).collect())
}
JsonValue::Object(obj) => JsonValue::Object(
obj.into_iter()
.map(|(k, v)| (k, normalize_json_value(v)))
.collect(),
),
_ => value,
}
}
#[cfg(test)]
fn values_match_with_multiline_tolerance(expected: &JsonValue, actual: &JsonValue) -> bool {
match (expected, actual) {
(JsonValue::String(exp_str), JsonValue::String(act_str)) => {
if exp_str.contains('\n') && act_str.contains('\n') {
let exp_normalized = exp_str
.lines()
.map(|line| line.trim_start())
.collect::<Vec<_>>()
.join("\n");
let act_normalized = act_str
.lines()
.map(|line| line.trim_start())
.collect::<Vec<_>>()
.join("\n");
exp_normalized == act_normalized || exp_str == act_str
} else {
exp_str == act_str
}
}
(JsonValue::Array(exp_arr), JsonValue::Array(act_arr)) => {
exp_arr.len() == act_arr.len()
&& exp_arr
.iter()
.zip(act_arr.iter())
.all(|(e, a)| values_match_with_multiline_tolerance(e, a))
}
(JsonValue::Object(exp_obj), JsonValue::Object(act_obj)) => {
exp_obj.len() == act_obj.len()
&& exp_obj.iter().all(|(key, exp_val)| {
act_obj.get(key).map_or(false, |act_val| {
values_match_with_multiline_tolerance(exp_val, act_val)
})
})
}
_ => expected == actual,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_standard_assertions() {
let test_file_path = "tests/assertions/mixed.json";
if !std::path::Path::new(test_file_path).exists() {
eprintln!(
"Warning: Standard test file {} not found. Run 'git submodule update --init' to initialize the test submodule.",
test_file_path
);
return;
}
let test_content = fs::read_to_string(test_file_path)
.expect("Failed to read standard assertion test file");
let tests: Vec<AssertionTest> =
serde_json::from_str(&test_content).expect("Failed to parse assertion test JSON");
let mut passed = 0;
let mut failed = 0;
let mut failed_tests = Vec::new();
for test in tests {
let result = parse_huml(&test.input);
let test_passed = if test.error {
result.is_err()
} else {
result.is_ok()
};
if test_passed {
passed += 1;
println!("✓ {}", test.name);
} else {
failed += 1;
failed_tests.push(test.name.clone());
if test.error {
println!(
"✗ {} - Expected error but parsing succeeded: {:?}",
test.name,
result.unwrap()
);
} else {
println!(
"✗ {} - Expected success but parsing failed: {:?}",
test.name,
result.unwrap_err()
);
}
}
}
println!("\nAssertion Test Results:");
println!("Passed: {}", passed);
println!("Failed: {}", failed);
if !failed_tests.is_empty() {
println!("Failed tests: {:?}", failed_tests);
}
if failed > 0 {
panic!(
"{} assertion tests failed. Failed tests: {:?}",
failed, failed_tests
);
}
}
#[test]
fn test_standard_documents() {
let huml_file_path = "tests/documents/mixed.huml";
let json_file_path = "tests/documents/mixed.json";
if !std::path::Path::new(huml_file_path).exists()
|| !std::path::Path::new(json_file_path).exists()
{
eprintln!(
"Warning: Standard test files not found. Run 'git submodule update --init' to initialize the test submodule."
);
return;
}
let huml_content =
fs::read_to_string(huml_file_path).expect("Failed to read HUML document test file");
let json_content =
fs::read_to_string(json_file_path).expect("Failed to read JSON reference file");
let huml_result = parse_huml(&huml_content);
assert!(
huml_result.is_ok(),
"Failed to parse HUML document: {:?}",
huml_result.unwrap_err()
);
let (_, huml_document) = huml_result.unwrap();
let huml_value = &huml_document.root;
let expected_json: JsonValue =
serde_json::from_str(&json_content).expect("Failed to parse reference JSON");
let actual_json = huml_to_json(huml_value);
let normalized_expected = normalize_json_value(expected_json);
let normalized_actual = normalize_json_value(actual_json);
if normalized_expected != normalized_actual {
if let (JsonValue::Object(expected_map), JsonValue::Object(actual_map)) =
(&normalized_expected, &normalized_actual)
{
let mut acceptable_differences = true;
for (key, expected_val) in expected_map {
if let Some(actual_val) = actual_map.get(key) {
if !values_match_with_multiline_tolerance(expected_val, actual_val) {
acceptable_differences = false;
break;
}
} else {
acceptable_differences = false;
break;
}
}
if !acceptable_differences || expected_map.len() != actual_map.len() {
println!("Expected JSON:");
println!(
"{}",
serde_json::to_string_pretty(&normalized_expected).unwrap()
);
println!("\nActual JSON:");
println!(
"{}",
serde_json::to_string_pretty(&normalized_actual).unwrap()
);
panic!("Document test failed - structures don't match");
} else {
println!(
"✓ Document test passed - HUML and JSON structures match (with acceptable multiline string differences)"
);
}
} else {
println!("Expected JSON:");
println!(
"{}",
serde_json::to_string_pretty(&normalized_expected).unwrap()
);
println!("\nActual JSON:");
println!(
"{}",
serde_json::to_string_pretty(&normalized_actual).unwrap()
);
panic!("Document test failed - structures don't match");
}
} else {
println!("✓ Document test passed - HUML and JSON structures match exactly");
}
}
#[test]
fn test_submodule_availability() {
let _submodule_path = "tests";
let readme_path = "tests/README.md";
if std::path::Path::new(readme_path).exists() {
println!("✓ HUML test submodule is available");
let readme_content =
fs::read_to_string(readme_path).expect("Failed to read submodule README");
assert!(readme_content.contains("HUML test data"));
println!("✓ Submodule README validation passed");
} else {
println!("Warning: HUML test submodule not initialized. Run:");
println!(" git submodule init");
println!(" git submodule update");
}
}
}