243 lines
7.9 KiB
Rust
243 lines
7.9 KiB
Rust
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<Option<Value>, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let v = Value::deserialize(deserializer)?;
|
|
Ok(Some(v))
|
|
}
|
|
|
|
// Type alias for easier reading
|
|
type CompiledSuite = Arc<Vec<(Suite, Arc<Result<Arc<crate::database::Database>, crate::drop::Drop>>)>>;
|
|
|
|
// Global cache mapping filename -> Vector of (Parsed JSON suite, Compiled Database)
|
|
static CACHE: OnceLock<RwLock<HashMap<String, CompiledSuite>>> = 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<Suite> = 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::<String>::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<String> = 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<String, 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();
|
|
|
|
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<String, String>, gen_map: &mut HashMap<String, usize>) -> 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<String> = 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();
|
|
}
|