use crate::tests::types::Suite; use serde::Deserialize; use serde_json::Value; use std::collections::HashMap; use std::fs; use std::sync::{Arc, OnceLock, RwLock}; pub fn deserialize_some<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { let v = Value::deserialize(deserializer)?; Ok(Some(v)) } // Type alias for easier reading type CompiledSuite = Arc, crate::drop::Drop>>)>>; // Global cache mapping filename -> Vector of (Parsed JSON suite, Compiled Database) static CACHE: OnceLock>> = OnceLock::new(); fn get_cached_file(path: &str) -> CompiledSuite { let cache_lock = CACHE.get_or_init(|| RwLock::new(HashMap::new())); let file_data = { let read_guard = cache_lock.read().unwrap(); read_guard.get(path).cloned() }; match file_data { Some(data) => data, None => { let mut write_guard = cache_lock.write().unwrap(); // double check in case another thread compiled while we waited for lock if let Some(data) = write_guard.get(path) { data.clone() } else { let content = fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read file: {}", path)); let suites: Vec = serde_json::from_str(&content) .unwrap_or_else(|e| panic!("Failed to parse JSON in {}: {}", path, e)); let mut compiled_suites = Vec::new(); for suite in suites { let (db, drop) = crate::database::Database::new(suite.database.clone()); let compiled_db = if drop.errors.is_empty() { Ok(Arc::new(db)) } else { Err(drop) }; compiled_suites.push((suite, Arc::new(compiled_db))); } let new_data = Arc::new(compiled_suites); write_guard.insert(path.to_string(), new_data.clone()); new_data } } } } pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<(), String> { let file_data = get_cached_file(path); if suite_idx >= file_data.len() { panic!("Suite Index {} out of bounds for file {}", suite_idx, path); } let (group, db) = &file_data[suite_idx]; if case_idx >= group.tests.len() { panic!( "Case Index {} out of bounds for suite {} in file {}", case_idx, suite_idx, path ); } let test = &group.tests[case_idx]; let mut failures = Vec::::new(); // For validate/merge/query, if setup failed we must structurally fail this test let db_unwrapped = if test.action.as_str() != "compile" { match &**db { Ok(valid_db) => Some(valid_db.clone()), Err(drop) => { let error_messages: Vec = drop .errors .iter() .map(|e| format!("Error {} at path {}: {}", e.code, e.details.path.as_deref().unwrap_or("/"), e.message)) .collect(); failures.push(format!( "[{}] Cannot run '{}' test '{}': System Setup Compilation structurally failed:\n{}", group.description, test.action, test.description, error_messages.join("\n") )); None } } } else { None }; if !failures.is_empty() { return Err(failures.join("\n")); } // 4. Run Tests match test.action.as_str() { "compile" => { let result = test.run_compile(db); if let Err(e) = result { println!("TEST COMPILE ERROR FOR '{}': {}", test.description, e); failures.push(format!( "[{}] Compile Test '{}' failed. Error: {}", group.description, test.description, e )); } } "validate" => { let result = test.run_validate(db_unwrapped.unwrap()); if let Err(e) = result { println!("TEST VALIDATE ERROR FOR '{}': {}", test.description, e); failures.push(format!( "[{}] Validate Test '{}' failed. Error: {}", group.description, test.description, e )); } } "merge" => { let result = test.run_merge(db_unwrapped.unwrap(), path, suite_idx, case_idx); if let Err(e) = result { println!("TEST MERGE ERROR FOR '{}': {}", test.description, e); failures.push(format!( "[{}] Merge Test '{}' failed. Error: {}", group.description, test.description, e )); } } "query" => { let result = test.run_query(db_unwrapped.unwrap(), path, suite_idx, case_idx); if let Err(e) = result { println!("TEST QUERY ERROR FOR '{}': {}", test.description, e); failures.push(format!( "[{}] Query Test '{}' failed. Error: {}", group.description, test.description, e )); } } _ => { failures.push(format!( "[{}] Unknown action '{}' for test '{}'", group.description, test.action, test.description )); } } if !failures.is_empty() { return Err(failures.join("\n")); } Ok(()) } pub fn extract_uuids(val: &Value, path: &str, map: &mut HashMap) { let uuid_re = regex::Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$").unwrap(); match val { Value::Object(obj) => { for (k, v) in obj { let new_path = if path.is_empty() { k.clone() } else { format!("{}.{}", path, k) }; extract_uuids(v, &new_path, map); } } Value::Array(arr) => { for (i, v) in arr.iter().enumerate() { let new_path = if path.is_empty() { i.to_string() } else { format!("{}.{}", path, i) }; extract_uuids(v, &new_path, map); } } Value::String(s) => { if s != "00000000-0000-0000-0000-000000000000" && uuid_re.is_match(s) { map.insert(s.clone(), path.to_string()); } } _ => {} } } pub fn canonicalize_with_map(s: &str, uuid_map: &HashMap, gen_map: &mut HashMap) -> String { let uuid_re = regex::Regex::new(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}").unwrap(); let s1 = uuid_re.replace_all(s, |caps: ®ex::Captures| { let val = &caps[0]; if val == "00000000-0000-0000-0000-000000000000" { val.to_string() } else if let Some(path) = uuid_map.get(val) { format!("{{{{uuid:{}}}}}", path) } else { let next_idx = gen_map.len(); let idx = *gen_map.entry(val.to_string()).or_insert(next_idx); format!("{{{{uuid:generated_{}}}}}", idx) } }); let ts_re = regex::Regex::new(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|\+\d{2}(?::\d{2})?)?").unwrap(); ts_re.replace_all(&s1, "{{timestamp}}").to_string() } pub fn update_sql_fixture(path: &str, suite_idx: usize, case_idx: usize, queries: &[String]) { use crate::tests::formatter::SqlFormatter; let content = fs::read_to_string(path).unwrap(); let mut file_data: Value = serde_json::from_str(&content).unwrap(); let mut uuid_map = HashMap::new(); if let Some(test_case) = file_data.get(suite_idx).and_then(|s| s.get("tests")).and_then(|t| t.get(case_idx)) { if let Some(data) = test_case.get("data") { extract_uuids(data, "data", &mut uuid_map); } if let Some(mocks) = test_case.get("mocks") { extract_uuids(mocks, "mocks", &mut uuid_map); } } let mut gen_map = HashMap::new(); let mut formatted_sql = Vec::new(); for q in queries { let res = SqlFormatter::format(q); let mapped_res: Vec = res.into_iter().map(|l| canonicalize_with_map(&l, &uuid_map, &mut gen_map)).collect(); formatted_sql.push(mapped_res); } if let Some(expect) = file_data[suite_idx]["tests"][case_idx].get_mut("expect") { if let Some(obj) = expect.as_object_mut() { obj.remove("pattern"); obj.insert("sql".to_string(), serde_json::json!(formatted_sql)); } } // To preserve original formatting, we just use serde_json pretty output let formatted_json = serde_json::to_string_pretty(&file_data).unwrap(); fs::write(path, formatted_json).unwrap(); }