use json_schema_ast::SchemaDocument;
use json_schema_fuzz::{GenerationConfig, ValueGenerator};
use jsoncompat::{Role, check_compat, explain_compat_failure};
use rand::{SeedableRng, rngs::StdRng};
use serde::Deserialize;
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
datatest_stable::harness! {
{ test = fixture, root = "tests/fixtures/backcompat", pattern = r".*[/\\]expect\.json$" },
}
#[derive(Deserialize)]
struct Expectation {
serializer: bool,
deserializer: bool,
#[serde(default)]
expected_serializer_message: Option<String>,
#[serde(default)]
expected_deserializer_message: Option<String>,
#[serde(default)]
allowed_failure: bool,
}
#[derive(Deserialize, Default)]
struct SampleSets {
#[serde(default)]
old_only: Vec<Value>,
#[serde(default)]
new_only: Vec<Value>,
#[serde(default)]
both: Vec<Value>,
}
fn fixture(expect_file: &Path) -> Result<(), Box<dyn std::error::Error>> {
let dir: PathBuf = expect_file.parent().unwrap().into();
let old_raw: Value = serde_json::from_slice(&fs::read(dir.join("old.json"))?)?;
let new_raw: Value = serde_json::from_slice(&fs::read(dir.join("new.json"))?)?;
let expect: Expectation = serde_json::from_slice(&fs::read(expect_file)?)?;
let old_schema = SchemaDocument::from_json(&old_raw)?;
let new_schema = SchemaDocument::from_json(&new_raw)?;
let ser = check_compat(&old_schema, &new_schema, Role::Serializer)?;
let de = check_compat(&old_schema, &new_schema, Role::Deserializer)?;
let ser_message = explain_compat_failure(&old_schema, &new_schema, Role::Serializer)?;
let de_message = explain_compat_failure(&old_schema, &new_schema, Role::Deserializer)?;
if expect.allowed_failure {
if ser == expect.serializer && de == expect.deserializer {
panic!("Previously-failing fixture now passes; remove allowed_failure from {dir:?}");
}
} else {
assert_eq!(ser, expect.serializer, "serializer mismatch in {dir:?}");
assert_eq!(de, expect.deserializer, "deserializer mismatch in {dir:?}");
}
assert_expected_message(
dir.as_path(),
"serializer",
ser,
expect.expected_serializer_message.as_deref(),
ser_message.as_deref(),
);
assert_expected_message(
dir.as_path(),
"deserializer",
de,
expect.expected_deserializer_message.as_deref(),
de_message.as_deref(),
);
let ex_path = dir.join("examples.json");
if ex_path.exists() {
let samples: SampleSets = serde_json::from_slice(&fs::read(&ex_path)?)?;
for v in &samples.old_only {
assert!(
old_schema.is_valid(v)?,
"old_only invalid in {dir:?}: {v:?}"
);
assert!(
!new_schema.is_valid(v)?,
"old_only accepted by NEW in {dir:?}: {v:?}"
);
}
for v in &samples.new_only {
assert!(
new_schema.is_valid(v)?,
"new_only invalid in {dir:?}: {v:?}"
);
assert!(
!old_schema.is_valid(v)?,
"new_only accepted by OLD in {dir:?}: {v:?}"
);
}
for v in &samples.both {
assert!(
old_schema.is_valid(v)? && new_schema.is_valid(v)?,
"both sample invalid in {dir:?}: {v:?}"
);
}
}
let mut rng = StdRng::seed_from_u64(0xDEADBEEF + dir.to_string_lossy().len() as u64);
let config = GenerationConfig::new(4);
for _ in 0..100 {
let v_new = ValueGenerator::generate(&new_schema, config, &mut rng)?;
if expect.serializer {
assert!(
old_schema.is_valid(&v_new)?,
"serializer=true but OLD rejects generated value {v_new:?} in {dir:?}"
);
}
let v_old = ValueGenerator::generate(&old_schema, config, &mut rng)?;
if expect.deserializer {
assert!(
new_schema.is_valid(&v_old)?,
"deserializer=true but NEW rejects generated value {v_old:?} in {dir:?}"
);
}
}
Ok(())
}
fn assert_expected_message(
dir: &Path,
role: &str,
compatible: bool,
expected: Option<&str>,
actual: Option<&str>,
) {
match (compatible, expected, actual) {
(true, None, None) => {}
(true, Some(expected), _) => panic!(
"compatible {role} fixture {dir:?} must not define a message, found {expected:?}"
),
(true, None, Some(actual)) => {
panic!("compatible {role} fixture {dir:?} produced an unexpected message {actual:?}")
}
(false, Some(expected), Some(actual)) => {
assert_eq!(actual, expected, "{role} issue message mismatch in {dir:?}")
}
(false, None, Some(actual)) => panic!(
"incompatible {role} fixture {dir:?} must define its expected message; actual {actual:?}"
),
(false, Some(expected), None) => panic!(
"incompatible {role} fixture {dir:?} expected {expected:?}, but no message was produced"
),
(false, None, None) => panic!(
"incompatible {role} fixture {dir:?} must define its expected message, but no message was produced"
),
}
}