403 lines
12 KiB
Rust
403 lines
12 KiB
Rust
use serde::Deserialize;
|
|
use std::fs;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct TestSuite {
|
|
#[allow(dead_code)]
|
|
description: String,
|
|
schema: Option<serde_json::Value>,
|
|
// Support JSPG-style test suites with explicit types/enums/puncs
|
|
types: Option<serde_json::Value>,
|
|
enums: Option<serde_json::Value>,
|
|
puncs: Option<serde_json::Value>,
|
|
tests: Vec<TestCase>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct TestCase {
|
|
description: String,
|
|
data: serde_json::Value,
|
|
valid: bool,
|
|
// Support explicit schema ID target for test case
|
|
schema_id: Option<String>,
|
|
// Expected output for masking tests
|
|
#[allow(dead_code)]
|
|
expected: Option<serde_json::Value>,
|
|
}
|
|
|
|
// use crate::registry::REGISTRY; // No longer used directly for tests!
|
|
use crate::validator::Validator;
|
|
use serde_json::Value;
|
|
|
|
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))
|
|
}
|
|
|
|
pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
|
|
let content =
|
|
fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read file: {}", path));
|
|
let suite: Vec<TestSuite> = serde_json::from_str(&content)
|
|
.unwrap_or_else(|e| panic!("Failed to parse JSON in {}: {}", path, e));
|
|
|
|
if index >= suite.len() {
|
|
panic!("Index {} out of bounds for file {}", index, path);
|
|
}
|
|
|
|
let group = &suite[index];
|
|
let mut failures = Vec::<String>::new();
|
|
|
|
// Create Local Registry for this test group
|
|
let mut registry = crate::registry::Registry::new();
|
|
|
|
// Helper to register items with 'schemas'
|
|
let register_schemas = |registry: &mut crate::registry::Registry, items_val: Option<&Value>| {
|
|
if let Some(val) = items_val {
|
|
if let Value::Array(arr) = val {
|
|
for item in arr {
|
|
if let Some(schemas_val) = item.get("schemas") {
|
|
if let Value::Array(schemas) = schemas_val {
|
|
for schema_val in schemas {
|
|
if let Ok(schema) =
|
|
serde_json::from_value::<crate::schema::Schema>(schema_val.clone())
|
|
{
|
|
registry.add(schema);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// 1. Register Family Schemas if 'types' is present
|
|
if let Some(types_val) = &group.types {
|
|
if let Value::Array(arr) = types_val {
|
|
let mut family_map: std::collections::HashMap<String, std::collections::HashSet<String>> =
|
|
std::collections::HashMap::new();
|
|
|
|
for item in arr {
|
|
if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
|
|
if let Some(hierarchy) = item.get("hierarchy").and_then(|v| v.as_array()) {
|
|
for ancestor in hierarchy {
|
|
if let Some(anc_str) = ancestor.as_str() {
|
|
family_map
|
|
.entry(anc_str.to_string())
|
|
.or_default()
|
|
.insert(name.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (family_name, members) in family_map {
|
|
let id = format!("{}.family", family_name);
|
|
let object_refs: Vec<Value> = members
|
|
.iter()
|
|
.map(|s| serde_json::json!({ "$ref": s }))
|
|
.collect();
|
|
|
|
let schema_json = serde_json::json!({
|
|
"$id": id,
|
|
"oneOf": object_refs
|
|
});
|
|
|
|
if let Ok(schema) = serde_json::from_value::<crate::schema::Schema>(schema_json) {
|
|
registry.add(schema);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Register items directly
|
|
register_schemas(&mut registry, group.enums.as_ref());
|
|
register_schemas(&mut registry, group.types.as_ref());
|
|
register_schemas(&mut registry, group.puncs.as_ref());
|
|
|
|
// 3. Register root 'schemas' if present (generic test support)
|
|
// Some tests use a raw 'schema' or 'schemas' field at the group level
|
|
if let Some(schema_val) = &group.schema {
|
|
match serde_json::from_value::<crate::schema::Schema>(schema_val.clone()) {
|
|
Ok(mut schema) => {
|
|
let id_clone = schema.obj.id.clone();
|
|
if id_clone.is_some() {
|
|
registry.add(schema);
|
|
} else {
|
|
// Fallback ID if none provided in schema
|
|
let id = format!("test:{}:{}", path, index);
|
|
schema.obj.id = Some(id);
|
|
registry.add(schema);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!(
|
|
"DEBUG: FAILED to deserialize group schema for index {}: {}",
|
|
index, e
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create Validator Instance (Takes ownership of registry)
|
|
let validator = Validator::new(registry);
|
|
|
|
// 4. Run Tests
|
|
for (_test_index, test) in group.tests.iter().enumerate() {
|
|
let mut schema_id = test.schema_id.clone();
|
|
|
|
// If no explicit schema_id, try to infer from the single schema in the group
|
|
if schema_id.is_none() {
|
|
if let Some(s) = &group.schema {
|
|
// If 'schema' is a single object, use its ID or "root"
|
|
if let Some(obj) = s.as_object() {
|
|
if let Some(id_val) = obj.get("$id") {
|
|
schema_id = id_val.as_str().map(|s| s.to_string());
|
|
}
|
|
}
|
|
if schema_id.is_none() {
|
|
schema_id = Some(format!("test:{}:{}", path, index));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default to the first punc if present (for puncs.json style)
|
|
if schema_id.is_none() {
|
|
if let Some(Value::Array(puncs)) = &group.puncs {
|
|
if let Some(first_punc) = puncs.first() {
|
|
if let Some(Value::Array(schemas)) = first_punc.get("schemas") {
|
|
if let Some(first_schema) = schemas.first() {
|
|
if let Some(id) = first_schema.get("$id").and_then(|v| v.as_str()) {
|
|
schema_id = Some(id.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(sid) = schema_id {
|
|
let result = validator.validate(&sid, &test.data);
|
|
|
|
if !result.errors.is_empty() != !test.valid {
|
|
failures.push(format!(
|
|
"[{}] Test '{}' failed. Expected: {}, Got: {}. Errors: {:?}",
|
|
group.description,
|
|
test.description,
|
|
test.valid,
|
|
!result.errors.is_empty(), // "Got Invalid?"
|
|
result.errors
|
|
));
|
|
}
|
|
} else {
|
|
failures.push(format!(
|
|
"[{}] Test '{}' skipped: No schema ID found.",
|
|
group.description, test.description
|
|
));
|
|
}
|
|
}
|
|
|
|
if !failures.is_empty() {
|
|
return Err(failures.join("\n"));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
pub fn run_test_file(path: &str) -> Result<(), String> {
|
|
let content =
|
|
fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read file: {}", path));
|
|
let suite: Vec<TestSuite> = serde_json::from_str(&content)
|
|
.unwrap_or_else(|e| panic!("Failed to parse JSON in {}: {}", path, e));
|
|
|
|
let mut failures = Vec::<String>::new();
|
|
for (group_index, group) in suite.into_iter().enumerate() {
|
|
// Create Isolated Registry for this test group
|
|
let mut registry = crate::registry::Registry::new();
|
|
|
|
// Helper to register items with 'schemas'
|
|
let register_schemas = |registry: &mut crate::registry::Registry, items_val: Option<Value>| {
|
|
if let Some(val) = items_val {
|
|
if let Value::Array(arr) = val {
|
|
for item in arr {
|
|
if let Some(schemas_val) = item.get("schemas") {
|
|
if let Value::Array(schemas) = schemas_val {
|
|
for schema_val in schemas {
|
|
if let Ok(schema) =
|
|
serde_json::from_value::<crate::schema::Schema>(schema_val.clone())
|
|
{
|
|
registry.add(schema);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// 1. Register Family Schemas if 'types' is present
|
|
if let Some(types_val) = &group.types {
|
|
if let Value::Array(arr) = types_val {
|
|
let mut family_map: std::collections::HashMap<String, std::collections::HashSet<String>> =
|
|
std::collections::HashMap::new();
|
|
|
|
for item in arr {
|
|
if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
|
|
// Default hierarchy contains self if not specified?
|
|
// Usually hierarchy is explicit in these tests.
|
|
if let Some(hierarchy) = item.get("hierarchy").and_then(|v| v.as_array()) {
|
|
for ancestor in hierarchy {
|
|
if let Some(anc_str) = ancestor.as_str() {
|
|
family_map
|
|
.entry(anc_str.to_string())
|
|
.or_default()
|
|
.insert(name.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (family_name, members) in family_map {
|
|
let id = format!("{}.family", family_name);
|
|
let object_refs: Vec<Value> = members
|
|
.into_iter()
|
|
.map(|s| serde_json::json!({ "$ref": s }))
|
|
.collect();
|
|
|
|
let schema_json = serde_json::json!({
|
|
"$id": id,
|
|
"oneOf": object_refs
|
|
});
|
|
|
|
if let Ok(schema) = serde_json::from_value::<crate::schema::Schema>(schema_json) {
|
|
registry.add(schema);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Register 'types', 'enums', and 'puncs' if present (JSPG style)
|
|
register_schemas(&mut registry, group.types);
|
|
register_schemas(&mut registry, group.enums);
|
|
register_schemas(&mut registry, group.puncs);
|
|
|
|
// Register main 'schema' if present (Standard style)
|
|
// Ensure ID is a valid URI to avoid Url::parse errors in Compiler
|
|
let unique_id = format!("test:{}:{}", path, group_index);
|
|
|
|
// Register main 'schema' if present (Standard style)
|
|
if let Some(ref schema_val) = group.schema {
|
|
let mut schema: crate::schema::Schema =
|
|
serde_json::from_value(schema_val.clone()).expect("Failed to parse test schema");
|
|
|
|
// If schema has no ID, assign unique_id and use add() or manual insert?
|
|
// Compiler needs ID. Registry::add needs ID.
|
|
if schema.obj.id.is_none() {
|
|
schema.obj.id = Some(unique_id.clone());
|
|
}
|
|
registry.add(schema);
|
|
}
|
|
|
|
// Create Instance (Takes Ownership)
|
|
let validator = Validator::new(registry);
|
|
|
|
for test in group.tests {
|
|
// Use explicit schema_id from test, or default to unique_id
|
|
let schema_id = test.schema_id.as_deref().unwrap_or(&unique_id).to_string();
|
|
|
|
let drop = validator.validate(&schema_id, &test.data);
|
|
|
|
if test.valid {
|
|
if !drop.errors.is_empty() {
|
|
let msg = format!(
|
|
"Test failed (expected valid): {}\nSchema: {:?}\nData: {:?}\nErrors: {:?}",
|
|
test.description,
|
|
group.schema, // We might need to find the actual schema used if schema_id is custom
|
|
test.data,
|
|
drop.errors
|
|
);
|
|
eprintln!("{}", msg);
|
|
failures.push(msg);
|
|
}
|
|
} else {
|
|
if drop.errors.is_empty() {
|
|
let msg = format!(
|
|
"Test failed (expected invalid): {}\nSchema: {:?}\nData: {:?}\nErrors: (Empty)",
|
|
test.description, group.schema, test.data
|
|
);
|
|
println!("{}", msg);
|
|
failures.push(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !failures.is_empty() {
|
|
return Err(format!(
|
|
"{} tests failed in file {}:\n\n{}",
|
|
failures.len(),
|
|
path,
|
|
failures.join("\n\n")
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn is_integer(v: &Value) -> bool {
|
|
match v {
|
|
Value::Number(n) => {
|
|
n.is_i64() || n.is_u64() || n.as_f64().filter(|n| n.fract() == 0.0).is_some()
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// serde_json treats 0 and 0.0 not equal. so we cannot simply use v1==v2
|
|
pub fn equals(v1: &Value, v2: &Value) -> bool {
|
|
// eprintln!("Comparing {:?} with {:?}", v1, v2);
|
|
match (v1, v2) {
|
|
(Value::Null, Value::Null) => true,
|
|
(Value::Bool(b1), Value::Bool(b2)) => b1 == b2,
|
|
(Value::Number(n1), Value::Number(n2)) => {
|
|
if let (Some(n1), Some(n2)) = (n1.as_u64(), n2.as_u64()) {
|
|
return n1 == n2;
|
|
}
|
|
if let (Some(n1), Some(n2)) = (n1.as_i64(), n2.as_i64()) {
|
|
return n1 == n2;
|
|
}
|
|
if let (Some(n1), Some(n2)) = (n1.as_f64(), n2.as_f64()) {
|
|
return (n1 - n2).abs() < f64::EPSILON;
|
|
}
|
|
false
|
|
}
|
|
(Value::String(s1), Value::String(s2)) => s1 == s2,
|
|
(Value::Array(arr1), Value::Array(arr2)) => {
|
|
if arr1.len() != arr2.len() {
|
|
return false;
|
|
}
|
|
arr1.iter().zip(arr2).all(|(e1, e2)| equals(e1, e2))
|
|
}
|
|
(Value::Object(obj1), Value::Object(obj2)) => {
|
|
if obj1.len() != obj2.len() {
|
|
return false;
|
|
}
|
|
for (k1, v1) in obj1 {
|
|
if let Some(v2) = obj2.get(k1) {
|
|
if !equals(v1, v2) {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|