use bumpalo::Bump;
use proptest::prelude::*;
use std::panic::{self, AssertUnwindSafe};
use seuil::clock::MockEnvironment;
use seuil::evaluator::engine::Evaluator;
use seuil::evaluator::value::Value;
use seuil::parser;
fn eval_with_limits(
expr: &str,
json_input: &serde_json::Value,
seed: u64,
) -> Result<String, String> {
let ast = parser::parse(expr).map_err(|e| format!("{e}"))?;
let arena = Bump::new();
let env = MockEnvironment::new(seed);
let chain_ast = parser::parse("function($f, $g) { function($x){ $g($f($x)) } }").ok();
let evaluator = Evaluator::new(&arena, &env, chain_ast, 50, Some(500));
let input = Value::from_json(&arena, json_input);
evaluator.bind_natives();
let result = evaluator
.evaluate(&ast, input)
.map_err(|e| format!("{e}"))?;
Ok(result.serialize(false))
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(5000))]
#[test]
fn parse_never_panics(input in "\\PC{0,200}") {
let result = panic::catch_unwind(AssertUnwindSafe(|| {
let _ = parser::parse(&input);
}));
prop_assert!(result.is_ok(), "Parser panicked on input: {:?}", input);
}
}
fn jsonata_expr_strategy() -> impl Strategy<Value = String> {
prop_oneof![
Just("null".to_string()),
Just("true".to_string()),
Just("false".to_string()),
(0i64..1000).prop_map(|n| n.to_string()),
"[a-z]{1,10}".prop_map(|s| format!("\"{}\"", s)),
"[a-z]{1,8}".prop_map(|s| s),
("[a-z]{1,5}", "[a-z]{1,5}").prop_map(|(a, b)| format!("{a}.{b}")),
(0i64..100, 1i64..100).prop_map(|(a, b)| format!("{a} + {b}")),
(0i64..100, 1i64..100).prop_map(|(a, b)| format!("{a} - {b}")),
(0i64..100, 1i64..100).prop_map(|(a, b)| format!("{a} * {b}")),
(0i64..100, 1i64..100).prop_map(|(a, b)| format!("{a} / {b}")),
"[a-z]{1,10}".prop_map(|s| format!("$length(\"{}\")", s)),
"[a-z]{1,10}".prop_map(|s| format!("$uppercase(\"{}\")", s)),
"[a-z]{1,10}".prop_map(|s| format!("$lowercase(\"{}\")", s)),
Just("[1, 2, 3]".to_string()),
Just("$sum([1, 2, 3])".to_string()),
Just("$count([1, 2, 3])".to_string()),
(0i64..1000).prop_map(|n| format!("$string({})", n)),
"[a-z]{1,10}".prop_map(|s| format!("$number(\"{}\")", s)),
]
}
fn json_value_strategy() -> impl Strategy<Value = serde_json::Value> {
prop_oneof![
Just(serde_json::Value::Null),
any::<bool>().prop_map(serde_json::Value::Bool),
(-1000i64..1000).prop_map(|n| serde_json::json!(n)),
"[a-z]{0,20}".prop_map(|s| serde_json::Value::String(s)),
Just(serde_json::json!({})),
Just(serde_json::json!([])),
Just(serde_json::json!({"name": "Alice", "age": 30})),
Just(serde_json::json!([1, 2, 3, 4, 5])),
Just(serde_json::json!({"items": [{"price": 10}, {"price": 20}]})),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(2000))]
#[test]
fn eval_never_panics(
expr in jsonata_expr_strategy(),
json in json_value_strategy(),
) {
let result = panic::catch_unwind(AssertUnwindSafe(|| {
let _ = eval_with_limits(&expr, &json, 42);
}));
prop_assert!(
result.is_ok(),
"Evaluator panicked on expr={:?}, input={:?}",
expr,
json
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(5000))]
#[test]
fn parse_arbitrary_bytes_never_panics(bytes in proptest::collection::vec(any::<u8>(), 0..200)) {
if let Ok(input) = std::str::from_utf8(&bytes) {
let result = panic::catch_unwind(AssertUnwindSafe(|| {
let _ = parser::parse(input);
}));
prop_assert!(result.is_ok(), "Parser panicked on bytes: {:?}", bytes);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn deterministic_evaluation(
expr in jsonata_expr_strategy(),
json in json_value_strategy(),
seed in 0u64..10000,
) {
let r1 = eval_with_limits(&expr, &json, seed);
let r2 = eval_with_limits(&expr, &json, seed);
match (&r1, &r2) {
(Ok(a), Ok(b)) => prop_assert_eq!(a, b, "Same seed must produce same result"),
(Err(a), Err(b)) => prop_assert_eq!(a, b, "Same seed must produce same error"),
_ => prop_assert!(false, "Determinism violation: first={:?}, second={:?}", r1, r2),
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn serialize_roundtrip(json in json_value_strategy()) {
let arena = Bump::new();
let value = Value::from_json(&arena, &json);
let serialized = value.serialize(false);
if value.is_undefined() {
return Ok(());
}
let reparsed: Result<serde_json::Value, _> = serde_json::from_str(&serialized);
if let Ok(reparsed) = reparsed {
prop_assert_eq!(
&json,
&reparsed,
"Serialize roundtrip mismatch: serialized to {:?}",
serialized,
);
}
}
}