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_native("42"), json!({"int": "42"}), &sc);
same(p::float_lit_native(3.5), json!({"float": 3.5}), &sc);
same(p::number_lit_native(42.0), json!(42), &sc);
same(p::number_lit_native(2.5), json!(2.5), &sc);
same(
p::r#ref_native(&["a", "b"], &sc),
json!({"ref": ["a", "b"]}),
&sc,
);
same(
p::ref_opt_native(&["n", "x"], &sc),
json!({"refOpt": ["n", "x"]}),
&sc,
);
same(
p::obj_native(vec![("x".into(), Value::Int(1))]),
json!({"obj": {"x": {"int": "1"}}}),
&sc,
);
same(
p::arr_native(vec![Value::Int(1), Value::Int(2)]),
json!({"arr": [{"int": "1"}, {"int": "2"}]}),
&sc,
);
both_fail(
p::obj_native(vec![("__proto__".into(), Value::Int(1))]),
json!({"obj": {"__proto__": {"int": "1"}}}),
);
}
#[test]
fn arithmetic_match_and_fail_closed() {
let e: &[(String, Value)] = &[];
same(
p::add_native(Value::Int(1), Value::Int(2)),
json!({"add": [{"int": "1"}, {"int": "2"}]}),
e,
);
same(
p::sub_native(Value::Int(5), Value::Int(3)),
json!({"sub": [{"int": "5"}, {"int": "3"}]}),
e,
);
same(
p::mul_native(Value::Int(4), Value::Int(6)),
json!({"mul": [{"int": "4"}, {"int": "6"}]}),
e,
);
same(
p::neg_native(Value::Int(9)),
json!({"neg": [{"int": "9"}]}),
e,
);
same(
p::div_native(Value::Int(7), Value::Int(2)),
json!({"div": [{"int": "7"}, {"int": "2"}]}),
e,
);
same(
p::mod_native(Value::Int(7), Value::Int(3)),
json!({"mod": [{"int": "7"}, {"int": "3"}]}),
e,
);
both_fail(
p::add_native(Value::Int(9223372036854775807), Value::Int(1)),
json!({"add": [{"int": "9223372036854775807"}, {"int": "1"}]}),
);
both_fail(
p::mod_native(Value::Int(1), Value::Int(0)),
json!({"mod": [{"int": "1"}, {"int": "0"}]}),
);
both_fail(
p::add_native(Value::Int(1), Value::Float(2.5)),
json!({"add": [{"int": "1"}, {"float": 2.5}]}),
);
both_fail(
p::div_native(Value::Int(9007199254740993), Value::Int(1)),
json!({"div": [{"int": "9007199254740993"}, {"int": "1"}]}),
);
}
#[test]
fn comparison_bool_len_match() {
let e: &[(String, Value)] = &[];
same(
p::eq_native(Value::Int(1), Value::Int(1)),
json!({"eq": [{"int": "1"}, {"int": "1"}]}),
e,
);
same(
p::ne_native(Value::Int(1), Value::Int(2)),
json!({"ne": [{"int": "1"}, {"int": "2"}]}),
e,
);
same(
p::lt_native(Value::Str("a".into()), Value::Str("b".into())),
json!({"lt": ["a", "b"]}),
e,
);
same(
p::le_native(Value::Str("a".into()), Value::Str("a".into())),
json!({"le": ["a", "a"]}),
e,
);
same(
p::gt_native(Value::Int(3), Value::Int(2)),
json!({"gt": [{"int": "3"}, {"int": "2"}]}),
e,
);
same(
p::ge_native(Value::Int(2), Value::Int(2)),
json!({"ge": [{"int": "2"}, {"int": "2"}]}),
e,
);
same(p::not_native(Value::Bool(true)), json!({"not": [true]}), e);
same(
p::len_native(Value::Arr(vec![Value::Bool(true), Value::Bool(false)])),
json!({"len": [{"arr": [true, false]}]}),
e,
);
both_fail(
p::len_native(Value::Str("nope".into())),
json!({"len": ["nope"]}),
);
both_fail(p::not_native(Value::Int(1)), json!({"not": [{"int": "1"}]}));
let sc = scope(&[("n", Value::Null)]);
both_fail_sc(
p::r#ref_native(&["n", "x"], &sc),
json!({"ref": ["n", "x"]}),
&sc,
);
}
#[test]
fn require_bool_native_fail_closed() {
assert!(p::require_bool_native(Value::Bool(true), "and").unwrap());
assert!(!p::require_bool_native(Value::Bool(false), "or").unwrap());
assert_eq!(
p::require_bool_native(Value::Int(1), "cond")
.expect_err("non-bool must fail")
.code
.as_str(),
"TYPE_MISMATCH",
);
}
#[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,
);
}