Skip to main content

loadmaster_probe/
check.rs

1//! Condition evaluation over captured exchanges. Pure: no I/O, clock or randomness.
2
3use serde_json::Value;
4
5use crate::client::Exchange;
6use crate::fixture::{Cond, Ref, Test};
7
8fn exchange(xs: &[Exchange], n: Option<usize>) -> Option<&Exchange> {
9    match n {
10        Some(i) => xs.get(i),
11        None => xs.last(),
12    }
13}
14
15/// JSONPath matches in an exchange's (reassembled) response.
16pub fn query(xs: &[Exchange], r: &Ref) -> Vec<Value> {
17    let Some(x) = exchange(xs, r.exchange) else {
18        return Vec::new();
19    };
20    match serde_json_path::JsonPath::parse(&r.path) {
21        Ok(p) => p.query(&x.response).all().into_iter().cloned().collect(),
22        Err(_) => Vec::new(),
23    }
24}
25
26/// Strings holding JSON compare by their parsed value; other values as they are.
27fn as_json(v: &Value) -> Value {
28    match v {
29        Value::String(s) => serde_json::from_str(s).unwrap_or_else(|_| v.clone()),
30        other => other.clone(),
31    }
32}
33
34pub fn eval(c: &Cond, xs: &[Exchange]) -> bool {
35    match c {
36        Cond::Always => true,
37        Cond::All(cs) => cs.iter().all(|c| eval(c, xs)),
38        Cond::Any(cs) => cs.iter().any(|c| eval(c, xs)),
39        Cond::Not(c) => !eval(c, xs),
40        Cond::Ran(n) => xs.len() > *n,
41        Cond::Status {
42            exchange: n,
43            code,
44            class,
45        } => exchange(xs, *n).is_some_and(|x| {
46            code.is_none_or(|c| x.status == c) && class.is_none_or(|k| x.status / 100 == k)
47        }),
48        Cond::StreamDone { exchange: n, done } => {
49            exchange(xs, *n).is_some_and(|x| x.stream.as_ref().is_some_and(|s| s.done == *done))
50        }
51        Cond::Same(a, b) => {
52            let (va, vb) = (query(xs, a), query(xs, b));
53            !va.is_empty()
54                && va.len() == vb.len()
55                && va
56                    .iter()
57                    .zip(&vb)
58                    .all(|(x, y)| !x.is_null() && as_json(x) == as_json(y))
59        }
60        Cond::Path(r, t) => {
61            let ms = query(xs, r);
62            match t {
63                Test::Exists(want) => ms.iter().any(|v| !v.is_null()) == *want,
64                Test::Count { min, max } => {
65                    let n = match ms.as_slice() {
66                        [Value::Array(a)] => a.len() as u64,
67                        [Value::Null] | [] => 0,
68                        many => many.len() as u64,
69                    };
70                    min.is_none_or(|m| n >= m) && max.is_none_or(|m| n <= m)
71                }
72                Test::Equals(v) => ms.first() == Some(v),
73                Test::ContainsAny(needles) => ms
74                    .iter()
75                    .filter_map(Value::as_str)
76                    .any(|s| needles.iter().any(|n| s.contains(n.as_str()))),
77                Test::Json(want) => {
78                    !ms.is_empty()
79                        && ms.iter().all(|v| {
80                            v.as_str()
81                                .and_then(|s| serde_json::from_str::<Value>(s).ok())
82                                .is_some()
83                        }) == *want
84                }
85                Test::Schema { schema, parse } => {
86                    let Ok(validator) = jsonschema::validator_for(schema) else {
87                        return false;
88                    };
89                    !ms.is_empty()
90                        && ms.iter().all(|v| {
91                            let inst = if *parse {
92                                v.as_str().and_then(|s| serde_json::from_str(s).ok())
93                            } else {
94                                Some(v.clone())
95                            };
96                            inst.is_some_and(|i| validator.is_valid(&i))
97                        })
98                }
99            }
100        }
101    }
102}