use behavior_contracts::primitives as p;
use behavior_contracts::{
deep_equals, evaluate_expression, hydrate_into, map_with_concurrency, skip_connection,
skip_single, unproduced_value, Value,
};
use serde_json::json;
fn scope(pairs: &[(&str, Value)]) -> Vec<(String, Value)> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
}
fn assert_value_eq(a: &Value, b: &Value) {
assert!(deep_equals(a, b), "value mismatch");
}
fn same(
actual: Result<Value, behavior_contracts::ExprFailure>,
node: serde_json::Value,
sc: &[(String, Value)],
) {
let expected = evaluate_expression(&node, sc);
match (actual, expected) {
(Ok(a), Ok(e)) => assert!(behavior_contracts::deep_equals(&a, &e), "value mismatch"),
(Err(a), Err(e)) => assert_eq!(a.code.as_str(), e.code.as_str()),
_ => panic!("ok/err disposition mismatch"),
}
}
fn both_fail(actual: Result<Value, behavior_contracts::ExprFailure>, node: serde_json::Value) {
both_fail_sc(actual, node, &[]);
}
fn both_fail_sc(
actual: Result<Value, behavior_contracts::ExprFailure>,
node: serde_json::Value,
sc: &[(String, Value)],
) {
let a = actual.expect_err("primitive did not fail");
let e = evaluate_expression(&node, sc).expect_err("evaluate did not fail");
assert_eq!(a.code.as_str(), e.code.as_str());
}
#[test]
fn literals_and_refs_match() {
let sc = scope(&[
("a", Value::Obj(vec![("b".into(), Value::Int(7))])),
("n", Value::Null),
]);
same(p::int_lit("42", &sc), json!({"int": "42"}), &sc);
same(p::float_lit(3.5, &sc), json!({"float": 3.5}), &sc);
same(p::r#ref(&["a", "b"], &sc), json!({"ref": ["a", "b"]}), &sc);
same(
p::ref_opt(&["n", "x"], &sc),
json!({"refOpt": ["n", "x"]}),
&sc,
);
same(
p::obj(&json!({"x": {"int": "1"}}), &sc),
json!({"obj": {"x": {"int": "1"}}}),
&sc,
);
same(
p::arr(&[json!({"int": "1"}), json!({"int": "2"})], &sc),
json!({"arr": [{"int": "1"}, {"int": "2"}]}),
&sc,
);
}
#[test]
fn arithmetic_match_and_fail_closed() {
let e: &[(String, Value)] = &[];
same(
p::add(&json!({"int": "1"}), &json!({"int": "2"}), e),
json!({"add": [{"int": "1"}, {"int": "2"}]}),
e,
);
same(
p::sub(&json!({"int": "5"}), &json!({"int": "3"}), e),
json!({"sub": [{"int": "5"}, {"int": "3"}]}),
e,
);
same(
p::mul(&json!({"int": "4"}), &json!({"int": "6"}), e),
json!({"mul": [{"int": "4"}, {"int": "6"}]}),
e,
);
same(
p::neg(&json!({"int": "9"}), e),
json!({"neg": [{"int": "9"}]}),
e,
);
same(
p::div(&json!({"int": "7"}), &json!({"int": "2"}), e),
json!({"div": [{"int": "7"}, {"int": "2"}]}),
e,
);
same(
p::r#mod(&json!({"int": "7"}), &json!({"int": "3"}), e),
json!({"mod": [{"int": "7"}, {"int": "3"}]}),
e,
);
both_fail(
p::add(
&json!({"int": "9223372036854775807"}),
&json!({"int": "1"}),
e,
),
json!({"add": [{"int": "9223372036854775807"}, {"int": "1"}]}),
);
both_fail(
p::r#mod(&json!({"int": "1"}), &json!({"int": "0"}), e),
json!({"mod": [{"int": "1"}, {"int": "0"}]}),
);
both_fail(
p::add(&json!({"int": "1"}), &json!({"float": 2.5}), e),
json!({"add": [{"int": "1"}, {"float": 2.5}]}),
);
both_fail(
p::div(&json!({"int": "9007199254740993"}), &json!({"int": "1"}), e),
json!({"div": [{"int": "9007199254740993"}, {"int": "1"}]}),
);
}
#[test]
fn comparison_concat_bool_cond_len_match() {
let e: &[(String, Value)] = &[];
same(
p::eq(&json!({"int": "1"}), &json!({"int": "1"}), e),
json!({"eq": [{"int": "1"}, {"int": "1"}]}),
e,
);
same(
p::ne(&json!({"int": "1"}), &json!({"int": "2"}), e),
json!({"ne": [{"int": "1"}, {"int": "2"}]}),
e,
);
same(
p::lt(&json!("a"), &json!("b"), e),
json!({"lt": ["a", "b"]}),
e,
);
same(
p::le(&json!("a"), &json!("a"), e),
json!({"le": ["a", "a"]}),
e,
);
same(
p::gt(&json!({"int": "3"}), &json!({"int": "2"}), e),
json!({"gt": [{"int": "3"}, {"int": "2"}]}),
e,
);
same(
p::ge(&json!({"int": "2"}), &json!({"int": "2"}), e),
json!({"ge": [{"int": "2"}, {"int": "2"}]}),
e,
);
same(
p::concat(&[json!("x"), json!("y"), json!("z")], e),
json!({"concat": ["x", "y", "z"]}),
e,
);
same(
p::and(&json!(true), &json!(false), e),
json!({"and": [true, false]}),
e,
);
same(
p::or(&json!(false), &json!(true), e),
json!({"or": [false, true]}),
e,
);
same(p::not(&json!(true), e), json!({"not": [true]}), e);
same(
p::coalesce(&json!(null), &json!("fb"), e),
json!({"coalesce": [null, "fb"]}),
e,
);
same(
p::cond(&json!(true), &json!("t"), &json!("e"), e),
json!({"cond": [true, "t", "e"]}),
e,
);
same(
p::len(&json!({"arr": [true, false]}), e),
json!({"len": [{"arr": [true, false]}]}),
e,
);
both_fail(
p::concat(&[json!("x"), json!({"int": "1"})], e),
json!({"concat": ["x", {"int": "1"}]}),
);
both_fail(p::len(&json!("nope"), e), json!({"len": ["nope"]}));
both_fail(
p::cond(&json!({"int": "1"}), &json!("t"), &json!("e"), e),
json!({"cond": [{"int": "1"}, "t", "e"]}),
);
let sc = scope(&[("n", Value::Null)]);
both_fail_sc(p::r#ref(&["n", "x"], &sc), json!({"ref": ["n", "x"]}), &sc);
}
#[test]
fn short_circuit_preserved() {
let e: &[(String, Value)] = &[];
assert_value_eq(
&p::and(&json!(false), &json!({"int": "bad"}), e).unwrap(),
&Value::Bool(false),
);
assert_value_eq(
&p::or(&json!(true), &json!({"int": "bad"}), e).unwrap(),
&Value::Bool(true),
);
assert_value_eq(
&p::cond(&json!(true), &json!("ok"), &json!({"int": "bad"}), e).unwrap(),
&Value::Str("ok".into()),
);
assert_value_eq(
&p::coalesce(&json!("present"), &json!({"int": "bad"}), e).unwrap(),
&Value::Str("present".into()),
);
}
#[test]
fn map_with_concurrency_order_and_bound() {
use std::sync::atomic::{AtomicUsize, Ordering};
let items: Vec<i64> = (0..8).collect();
let in_flight = AtomicUsize::new(0);
let max_in_flight = AtomicUsize::new(0);
let out = map_with_concurrency(&items, 3, |x, _i| {
let cur = in_flight.fetch_add(1, Ordering::SeqCst) + 1;
max_in_flight.fetch_max(cur, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(5));
in_flight.fetch_sub(1, Ordering::SeqCst);
x * 10
});
assert_eq!(out, items.iter().map(|x| x * 10).collect::<Vec<_>>()); assert!(max_in_flight.load(Ordering::SeqCst) <= 3);
}
#[test]
fn map_with_concurrency_degenerate() {
assert_eq!(
map_with_concurrency(&[1, 2, 3], 1, |x, _i| x + 1),
vec![2, 3, 4]
);
let empty: Vec<i64> = vec![];
assert_eq!(
map_with_concurrency(&empty, 4, |x, _i| *x),
Vec::<i64>::new()
);
}
#[test]
fn skip_unproduced_helpers() {
let conn = Value::Obj(vec![
("items".into(), Value::Arr(vec![])),
("cursor".into(), Value::Null),
]);
assert_value_eq(
&unproduced_value(Some(behavior_contracts::RelationKind::Single)),
&Value::Null,
);
assert_value_eq(
&unproduced_value(Some(behavior_contracts::RelationKind::Connection)),
&conn,
);
assert_value_eq(&unproduced_value(None), &Value::Null);
assert_value_eq(&skip_single(), &Value::Null);
assert_value_eq(&skip_connection(), &conn);
}
#[test]
fn hydrate_into_zip_attach() {
let over = vec![
Value::Obj(vec![("id".into(), Value::Int(1))]),
Value::Obj(vec![("id".into(), Value::Int(2))]),
Value::Obj(vec![("id".into(), Value::Int(3))]),
];
let out = hydrate_into(
&over,
"tag",
&[0, 2],
&[Value::Str("A".into()), Value::Str("C".into())],
)
.unwrap();
let expected = [
Value::Obj(vec![
("id".into(), Value::Int(1)),
("tag".into(), Value::Str("A".into())),
]),
Value::Obj(vec![("id".into(), Value::Int(2))]),
Value::Obj(vec![
("id".into(), Value::Int(3)),
("tag".into(), Value::Str("C".into())),
]),
];
assert_eq!(out.len(), expected.len());
for (a, b) in out.iter().zip(expected.iter()) {
assert_value_eq(a, b);
}
let bad = hydrate_into(&[Value::Int(42)], "tag", &[0], &[Value::Str("x".into())]);
assert!(bad.is_err());
}
#[test]
fn ref_native_matches_evaluate() {
let sc = scope(&[(
"obj",
Value::Obj(vec![
(
"a".into(),
Value::Obj(vec![("b".into(), Value::Str("deep".into()))]),
),
("nil".into(), Value::Null),
("scalar".into(), Value::Int(7)),
]),
)]);
same(
p::ref_native(&["obj", "a", "b"], &sc),
json!({"ref":["obj","a","b"]}),
&sc,
);
same(
p::ref_native(&["missing"], &sc),
json!({"ref":["missing"]}),
&sc,
);
same(
p::ref_native(&["obj", "a", "zzz"], &sc),
json!({"ref":["obj","a","zzz"]}),
&sc,
);
same(
p::ref_native(&["obj", "nil", "x"], &sc),
json!({"ref":["obj","nil","x"]}),
&sc,
);
same(
p::ref_native(&["obj", "scalar", "x"], &sc),
json!({"ref":["obj","scalar","x"]}),
&sc,
);
same(
p::ref_opt_native(&["obj", "nil", "x"], &sc),
json!({"refOpt":["obj","nil","x"]}),
&sc,
);
same(
p::ref_opt_native(&["obj", "a", "b"], &sc),
json!({"refOpt":["obj","a","b"]}),
&sc,
);
}
#[test]
fn concat_native_matches_evaluate() {
let sc = scope(&[]);
same(
p::concat_native(&[
Value::Str("a".into()),
Value::Str("b".into()),
Value::Str("c".into()),
]),
json!({"concat":["a","b","c"]}),
&sc,
);
same(
p::concat_native(&[Value::Str("a".into()), Value::Int(1)]),
json!({"concat":["a",{"int":"1"}]}),
&sc,
);
same(
p::concat_native(&[Value::Str("a".into())]),
json!({"concat":["a"]}),
&sc,
);
}