• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gripmock / grpctestify-rust / 24582789334

17 Apr 2026 07:23PM UTC coverage: 76.58% (+0.1%) from 76.438%
24582789334

push

github

web-flow
Merge pull request #39 from gripmock/refactoring-v3

refactoring part3

579 of 719 new or added lines in 11 files covered. (80.53%)

5 existing lines in 3 files now uncovered.

17585 of 22963 relevant lines covered (76.58%)

2430.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.12
/src/assert/operators.rs
1
// AST-based assertion engine
2
// All evaluation goes through the AssertionExpr AST — no string-based parsing.
3

4
use anyhow::Result;
5
use regex::Regex;
6
use serde_json::Value;
7
use std::cell::RefCell;
8
use std::collections::HashMap;
9
use std::rc::Rc;
10

11
use crate::assert::engine::AssertionResult;
12
use crate::parser::assertion_ast::{AssertionExpr, BinaryOp, Expr, Literal, parse_assertion};
13
use crate::plugins::{
14
    AssertionTiming, PluginContext, PluginManager, PluginResult, normalize_plugin_name,
15
};
16

17
thread_local! {
18
    static REGEX_CACHE: RefCell<HashMap<String, std::result::Result<Rc<Regex>, String>>> =
19
        RefCell::new(HashMap::new());
20
}
21

22
fn cached_regex(pattern: &str) -> std::result::Result<Rc<Regex>, String> {
10✔
23
    if let Some(cached) = REGEX_CACHE.with(|cache| cache.borrow().get(pattern).cloned()) {
10✔
24
        return cached;
4✔
25
    }
6✔
26

27
    let compiled = Regex::new(pattern)
6✔
28
        .map(Rc::new)
6✔
29
        .map_err(|err| err.to_string());
6✔
30

31
    REGEX_CACHE.with(|cache| {
6✔
32
        cache
6✔
33
            .borrow_mut()
6✔
34
            .insert(pattern.to_string(), compiled.clone());
6✔
35
    });
6✔
36

37
    compiled
6✔
38
}
10✔
39

40
/// Evaluate an assertion expression.
41
/// Returns `Ok(Some(result))` when the AST engine handled the expression,
42
/// `Ok(None)` when the expression should fall through to the JQ evaluator.
43
pub fn evaluate_assertion(
120✔
44
    plugin_manager: &PluginManager,
120✔
45
    assertion: &str,
120✔
46
    response: &Value,
120✔
47
    headers: Option<&HashMap<String, String>>,
120✔
48
    trailers: Option<&HashMap<String, String>>,
120✔
49
    timing: Option<&AssertionTiming>,
120✔
50
) -> Result<Option<AssertionResult>> {
120✔
51
    let trimmed = assertion.trim();
120✔
52
    if trimmed.is_empty() {
120✔
NEW
53
        return Ok(None);
×
54
    }
120✔
55

56
    let ast = parse_assertion(trimmed);
120✔
57
    match &ast {
120✔
58
        AssertionExpr::Raw(_) => Ok(None),
2✔
59
        _ => evaluate_ast(plugin_manager, &ast, response, headers, trailers, timing).map(Some),
118✔
60
    }
61
}
120✔
62

63
fn evaluate_ast(
151✔
64
    pm: &PluginManager,
151✔
65
    expr: &AssertionExpr,
151✔
66
    response: &Value,
151✔
67
    headers: Option<&HashMap<String, String>>,
151✔
68
    trailers: Option<&HashMap<String, String>>,
151✔
69
    timing: Option<&AssertionTiming>,
151✔
70
) -> Result<AssertionResult> {
151✔
71
    match expr {
151✔
72
        AssertionExpr::Not(inner) => {
7✔
73
            let r = evaluate_ast(pm, inner, response, headers, trailers, timing)?;
7✔
74
            Ok(negate(r))
7✔
75
        }
76
        AssertionExpr::NotNot(inner) => {
2✔
77
            evaluate_ast(pm, inner, response, headers, trailers, timing)
2✔
78
        }
79
        AssertionExpr::And { left, right } => {
3✔
80
            let lr = evaluate_ast(pm, left, response, headers, trailers, timing)?;
3✔
81
            if !is_pass(&lr) {
3✔
NEW
82
                return Ok(AssertionResult::fail(format!(
×
NEW
83
                    "Left of 'and' failed: {}",
×
NEW
84
                    fmt_result_short(&lr)
×
NEW
85
                )));
×
86
            }
3✔
87
            let rr = evaluate_ast(pm, right, response, headers, trailers, timing)?;
3✔
88
            if !is_pass(&rr) {
3✔
89
                return Ok(AssertionResult::fail(format!(
1✔
90
                    "Right of 'and' failed: {}",
1✔
91
                    fmt_result_short(&rr)
1✔
92
                )));
1✔
93
            }
2✔
94
            Ok(AssertionResult::Pass)
2✔
95
        }
96
        AssertionExpr::Or { left, right } => {
6✔
97
            let lr = evaluate_ast(pm, left, response, headers, trailers, timing)?;
6✔
98
            if is_pass(&lr) {
6✔
99
                return Ok(AssertionResult::Pass);
3✔
100
            }
3✔
101
            let rr = evaluate_ast(pm, right, response, headers, trailers, timing)?;
3✔
102
            if is_pass(&rr) {
3✔
103
                return Ok(AssertionResult::Pass);
1✔
104
            }
2✔
105
            Ok(AssertionResult::fail(format!(
2✔
106
                "Both sides of 'or' failed: left={}, right={}",
2✔
107
                fmt_result_short(&lr),
2✔
108
                fmt_result_short(&rr)
2✔
109
            )))
2✔
110
        }
111
        AssertionExpr::Xor { left, right } => {
3✔
112
            let lr = evaluate_ast(pm, left, response, headers, trailers, timing)?;
3✔
113
            let rr = evaluate_ast(pm, right, response, headers, trailers, timing)?;
3✔
114
            let lp = is_pass(&lr);
3✔
115
            let rp = is_pass(&rr);
3✔
116
            if lp != rp {
3✔
117
                Ok(AssertionResult::Pass)
1✔
118
            } else {
119
                Ok(AssertionResult::fail(format!(
2✔
120
                    "Xor expects exactly one true, got left={} right={}",
2✔
121
                    lp, rp
2✔
122
                )))
2✔
123
            }
124
        }
125
        AssertionExpr::Binary { op, left, right } => {
56✔
126
            let lhs = eval_value(pm, left, response, headers, trailers, timing);
56✔
127
            let rhs = eval_value(pm, right, response, headers, trailers, timing);
56✔
128
            compare(lhs, op, rhs, left, right)
56✔
129
        }
130
        AssertionExpr::Paren(inner) => evaluate_ast(pm, inner, response, headers, trailers, timing),
3✔
131
        AssertionExpr::IfThenElse {
NEW
132
            condition,
×
NEW
133
            then_branch,
×
NEW
134
            else_branch,
×
135
        } => {
NEW
136
            let cond = evaluate_ast(pm, condition, response, headers, trailers, timing)?;
×
NEW
137
            if is_pass(&cond) {
×
NEW
138
                evaluate_ast(pm, then_branch, response, headers, trailers, timing)
×
139
            } else {
NEW
140
                evaluate_ast(pm, else_branch, response, headers, trailers, timing)
×
141
            }
142
        }
143
        AssertionExpr::Atom(_) => {
144
            if let AssertionExpr::Atom(Expr::PluginCall { name, args }) = expr {
71✔
145
                eval_plugin_as_assertion(pm, name, args, response, headers, trailers, timing)
71✔
146
            } else {
NEW
147
                let val = eval_value(pm, expr, response, headers, trailers, timing);
×
NEW
148
                if is_truthy(&val) {
×
NEW
149
                    Ok(AssertionResult::Pass)
×
150
                } else {
NEW
151
                    Ok(AssertionResult::fail(format!(
×
NEW
152
                        "Expression evaluated to falsy: {:?}",
×
NEW
153
                        val
×
NEW
154
                    )))
×
155
                }
156
            }
157
        }
NEW
158
        AssertionExpr::Raw(_) => Ok(AssertionResult::Error("Unparsed expression".into())),
×
159
    }
160
}
151✔
161

162
/// Evaluate an AST node as a JSON value (for use inside Binary, plugin args, etc.)
163
fn eval_plugin_as_assertion(
71✔
164
    pm: &PluginManager,
71✔
165
    name: &str,
71✔
166
    args: &[AssertionExpr],
71✔
167
    response: &Value,
71✔
168
    headers: Option<&HashMap<String, String>>,
71✔
169
    trailers: Option<&HashMap<String, String>>,
71✔
170
    timing: Option<&AssertionTiming>,
71✔
171
) -> Result<AssertionResult> {
71✔
172
    let func_name = format!("@{}", name);
71✔
173
    let resolved_name = normalize_plugin_name(&func_name);
71✔
174
    if let Some(plugin) = pm.get(resolved_name) {
71✔
175
        let ctx = PluginContext::new(response)
71✔
176
            .with_headers(headers)
71✔
177
            .with_trailers(trailers)
71✔
178
            .with_timing(timing);
71✔
179
        let arg_values: Vec<Value> = args
71✔
180
            .iter()
71✔
181
            .map(|a| eval_value(pm, a, response, headers, trailers, timing))
71✔
182
            .collect();
71✔
183
        match plugin.execute(&arg_values, &ctx) {
71✔
184
            Ok(PluginResult::Assertion(res)) => Ok(res),
58✔
185
            Ok(PluginResult::Value(val)) => {
13✔
186
                if is_truthy(&val) {
13✔
187
                    Ok(AssertionResult::Pass)
7✔
188
                } else {
189
                    Ok(AssertionResult::fail(format!(
6✔
190
                        "Plugin {} returned falsy value: {:?}",
6✔
191
                        resolved_name, val
6✔
192
                    )))
6✔
193
                }
194
            }
NEW
195
            Err(e) => Ok(AssertionResult::Error(format!("Plugin error: {}", e))),
×
196
        }
197
    } else {
NEW
198
        Ok(AssertionResult::Error(format!("Unknown plugin: {}", name)))
×
199
    }
200
}
71✔
201

202
fn eval_value(
196✔
203
    pm: &PluginManager,
196✔
204
    expr: &AssertionExpr,
196✔
205
    response: &Value,
196✔
206
    headers: Option<&HashMap<String, String>>,
196✔
207
    trailers: Option<&HashMap<String, String>>,
196✔
208
    timing: Option<&AssertionTiming>,
196✔
209
) -> Value {
196✔
210
    match expr {
196✔
211
        AssertionExpr::Atom(atom) => eval_atom(pm, atom, response, headers, trailers, timing),
196✔
NEW
212
        AssertionExpr::Paren(inner) => eval_value(pm, inner, response, headers, trailers, timing),
×
NEW
213
        AssertionExpr::Not(inner) => {
×
NEW
214
            let v = eval_value(pm, inner, response, headers, trailers, timing);
×
NEW
215
            Value::Bool(!is_truthy(&v))
×
216
        }
NEW
217
        AssertionExpr::NotNot(inner) => eval_value(pm, inner, response, headers, trailers, timing),
×
NEW
218
        AssertionExpr::And { left, right } => {
×
NEW
219
            let lv = eval_value(pm, left, response, headers, trailers, timing);
×
NEW
220
            if !is_truthy(&lv) {
×
NEW
221
                return Value::Bool(false);
×
UNCOV
222
            }
×
NEW
223
            let rv = eval_value(pm, right, response, headers, trailers, timing);
×
NEW
224
            Value::Bool(is_truthy(&rv))
×
225
        }
NEW
226
        AssertionExpr::Or { left, right } => {
×
NEW
227
            let lv = eval_value(pm, left, response, headers, trailers, timing);
×
NEW
228
            if is_truthy(&lv) {
×
NEW
229
                return Value::Bool(true);
×
UNCOV
230
            }
×
NEW
231
            let rv = eval_value(pm, right, response, headers, trailers, timing);
×
NEW
232
            Value::Bool(is_truthy(&rv))
×
233
        }
NEW
234
        AssertionExpr::Xor { left, right } => {
×
NEW
235
            let lv = eval_value(pm, left, response, headers, trailers, timing);
×
NEW
236
            let rv = eval_value(pm, right, response, headers, trailers, timing);
×
NEW
237
            Value::Bool(is_truthy(&lv) != is_truthy(&rv))
×
238
        }
NEW
239
        AssertionExpr::Binary { op, left, right } => {
×
NEW
240
            let lhs = eval_value(pm, left, response, headers, trailers, timing);
×
NEW
241
            let rhs = eval_value(pm, right, response, headers, trailers, timing);
×
NEW
242
            eval_binary_value(lhs, op, rhs)
×
243
        }
244
        AssertionExpr::IfThenElse {
NEW
245
            condition,
×
NEW
246
            then_branch,
×
NEW
247
            else_branch,
×
248
        } => {
NEW
249
            let cv = eval_value(pm, condition, response, headers, trailers, timing);
×
NEW
250
            if is_truthy(&cv) {
×
NEW
251
                eval_value(pm, then_branch, response, headers, trailers, timing)
×
252
            } else {
NEW
253
                eval_value(pm, else_branch, response, headers, trailers, timing)
×
254
            }
255
        }
NEW
256
        AssertionExpr::Raw(s) => resolve_path(s, response),
×
257
    }
258
}
196✔
259

260
fn eval_atom(
196✔
261
    pm: &PluginManager,
196✔
262
    atom: &Expr,
196✔
263
    response: &Value,
196✔
264
    headers: Option<&HashMap<String, String>>,
196✔
265
    trailers: Option<&HashMap<String, String>>,
196✔
266
    timing: Option<&AssertionTiming>,
196✔
267
) -> Value {
196✔
268
    match atom {
196✔
269
        Expr::JqPath(p) => resolve_path(p, response),
118✔
270
        Expr::PluginCall { name, args } => {
13✔
271
            let func_name = format!("@{}", name);
13✔
272
            let resolved_name = normalize_plugin_name(&func_name);
13✔
273
            if let Some(plugin) = pm.get(resolved_name) {
13✔
274
                let ctx = PluginContext::new(response)
13✔
275
                    .with_headers(headers)
13✔
276
                    .with_trailers(trailers)
13✔
277
                    .with_timing(timing);
13✔
278
                let arg_values: Vec<Value> = args
13✔
279
                    .iter()
13✔
280
                    .map(|a| eval_value(pm, a, response, headers, trailers, timing))
13✔
281
                    .collect();
13✔
282
                match plugin.execute(&arg_values, &ctx) {
13✔
283
                    Ok(PluginResult::Value(v)) => v,
13✔
NEW
284
                    Ok(PluginResult::Assertion(AssertionResult::Pass)) => Value::Bool(true),
×
NEW
285
                    Ok(PluginResult::Assertion(AssertionResult::Fail { .. })) => Value::Bool(false),
×
NEW
286
                    Ok(PluginResult::Assertion(AssertionResult::Error(e))) => {
×
NEW
287
                        Value::String(format!("error: {}", e))
×
288
                    }
NEW
289
                    Err(_) => Value::Null,
×
290
                }
291
            } else {
NEW
292
                Value::Null
×
293
            }
294
        }
295
        Expr::Literal(lit) => match lit {
65✔
296
            Literal::Bool(b) => Value::Bool(*b),
3✔
297
            Literal::Number(n) => n
33✔
298
                .parse::<i64>()
33✔
299
                .map(|i| Value::Number(serde_json::Number::from(i)))
33✔
300
                .unwrap_or_else(|_| {
33✔
NEW
301
                    n.parse::<f64>()
×
NEW
302
                        .ok()
×
NEW
303
                        .and_then(serde_json::Number::from_f64)
×
NEW
304
                        .map(Value::Number)
×
NEW
305
                        .unwrap_or(Value::Null)
×
NEW
306
                }),
×
307
            Literal::Str(s) => Value::String(s.clone()),
29✔
NEW
308
            Literal::Null => Value::Null,
×
309
        },
NEW
310
        Expr::Variable(name) => Value::String(format!("{{{{{}}}}}", name)),
×
NEW
311
        Expr::RegExp { pattern, flags: _ } => Value::String(format!("/{}/", pattern)),
×
NEW
312
        Expr::Json(s) | Expr::Yaml(s) => serde_json::from_str(s).unwrap_or(Value::Null),
×
313
    }
314
}
196✔
315

316
fn eval_binary_value(lhs: Value, op: &BinaryOp, rhs: Value) -> Value {
56✔
317
    let pass = match op {
56✔
318
        BinaryOp::Eq => lhs == rhs,
40✔
319
        BinaryOp::Ne => lhs != rhs,
1✔
320
        BinaryOp::Gt => compare_numeric(&lhs, &rhs, ">").unwrap_or(false),
2✔
321
        BinaryOp::Lt => compare_numeric(&lhs, &rhs, "<").unwrap_or(false),
2✔
322
        BinaryOp::Ge => compare_numeric(&lhs, &rhs, ">=").unwrap_or(false),
1✔
323
        BinaryOp::Le => compare_numeric(&lhs, &rhs, "<=").unwrap_or(false),
1✔
324
        BinaryOp::Contains => match (&lhs, &rhs) {
3✔
325
            (Value::String(l), Value::String(r)) => l.contains(r),
2✔
326
            (Value::Array(l), r) => l.contains(r),
1✔
327
            (Value::Object(l), Value::String(r)) => l.contains_key(r),
×
328
            _ => false,
×
329
        },
330
        BinaryOp::StartsWith => match (&lhs, &rhs) {
1✔
331
            (Value::String(l), Value::String(r)) => l.starts_with(r),
1✔
332
            _ => false,
×
333
        },
334
        BinaryOp::EndsWith => match (&lhs, &rhs) {
1✔
335
            (Value::String(l), Value::String(r)) => l.ends_with(r),
1✔
336
            _ => false,
×
337
        },
338
        BinaryOp::Matches => match (&lhs, &rhs) {
4✔
339
            (Value::String(l), Value::String(r)) => {
4✔
340
                cached_regex(r).map(|re| re.is_match(l)).unwrap_or(false)
4✔
341
            }
342
            _ => false,
×
343
        },
344
    };
345
    Value::Bool(pass)
56✔
346
}
56✔
347

348
fn compare(
56✔
349
    lhs: Value,
56✔
350
    op: &BinaryOp,
56✔
351
    rhs: Value,
56✔
352
    left_expr: &AssertionExpr,
56✔
353
    right_expr: &AssertionExpr,
56✔
354
) -> Result<AssertionResult> {
56✔
355
    if let BinaryOp::Matches = op
56✔
356
        && let (Value::String(_l), Value::String(r)) = (&lhs, &rhs)
4✔
357
        && cached_regex(r).is_err()
4✔
358
    {
NEW
359
        return Ok(AssertionResult::Error(format!("Invalid regex: {}", r)));
×
360
    }
56✔
361
    let pass = eval_binary_value(lhs.clone(), op, rhs.clone());
56✔
362
    if pass == Value::Bool(true) {
56✔
363
        Ok(AssertionResult::Pass)
43✔
364
    } else {
365
        Ok(AssertionResult::Fail {
13✔
366
            message: format!(
13✔
367
                "Assertion failed: {} {} {} (Values: {:?} vs {:?})",
13✔
368
                left_expr,
13✔
369
                op.as_str(),
13✔
370
                right_expr,
13✔
371
                lhs,
13✔
372
                rhs
13✔
373
            ),
13✔
374
            expected: Some(format!("{} {:?}", op.as_str(), rhs)),
13✔
375
            actual: Some(format!("{:?}", lhs)),
13✔
376
        })
13✔
377
    }
378
}
56✔
379

380
fn compare_numeric(lhs: &Value, rhs: &Value, op: &str) -> Option<bool> {
11✔
381
    let lhs_num = lhs.as_number()?;
11✔
382
    let rhs_num = rhs.as_number()?;
11✔
383

384
    let lhs_i = lhs_num
10✔
385
        .as_i64()
10✔
386
        .map(i128::from)
10✔
387
        .or_else(|| lhs_num.as_u64().map(i128::from));
10✔
388
    let rhs_i = rhs_num
10✔
389
        .as_i64()
10✔
390
        .map(i128::from)
10✔
391
        .or_else(|| rhs_num.as_u64().map(i128::from));
10✔
392

393
    if let (Some(l), Some(r)) = (lhs_i, rhs_i) {
10✔
394
        return Some(match op {
10✔
395
            ">" => l > r,
10✔
396
            "<" => l < r,
7✔
397
            ">=" => l >= r,
4✔
398
            "<=" => l <= r,
2✔
NEW
399
            _ => return None,
×
400
        });
NEW
401
    }
×
402

NEW
403
    let (l, r) = (lhs_num.as_f64()?, rhs_num.as_f64()?);
×
NEW
404
    Some(match op {
×
NEW
405
        ">" => l > r,
×
NEW
406
        "<" => l < r,
×
NEW
407
        ">=" => l >= r,
×
NEW
408
        "<=" => l <= r,
×
NEW
409
        _ => return None,
×
410
    })
411
}
11✔
412

413
fn resolve_path(path: &str, root: &Value) -> Value {
122✔
414
    if path == "." {
122✔
415
        return root.clone();
×
416
    }
122✔
417
    if path.is_empty() {
122✔
NEW
418
        return Value::Null;
×
419
    }
122✔
420
    if !path.starts_with('.') && !path.starts_with('$') {
122✔
NEW
421
        return Value::String(path.to_string());
×
422
    }
122✔
423
    eval_jaq_one(path, root).unwrap_or(Value::Null)
122✔
424
}
122✔
425

426
fn eval_jaq_one(expr: &str, input: &Value) -> anyhow::Result<Value> {
122✔
427
    use jaq_core::defs as core_defs;
428
    use jaq_core::funs as core_funs;
429
    use jaq_core::{Compiler, Ctx, Vars, data, load, unwrap_valr};
430
    use jaq_json::Val as JaqVal;
431

432
    let arena = load::Arena::default();
122✔
433
    let defs = core_defs().chain(jaq_std::defs()).chain(jaq_json::defs());
122✔
434
    let funs = core_funs().chain(jaq_std::funs()).chain(jaq_json::funs());
122✔
435
    let loader = load::Loader::new(defs);
122✔
436
    let program = load::File {
122✔
437
        code: expr,
122✔
438
        path: (),
122✔
439
    };
122✔
440

441
    let modules = loader
122✔
442
        .load(&arena, program)
122✔
443
        .map_err(|errs| anyhow::anyhow!("JQ parse error: {:?}", errs))?;
122✔
444

445
    let filter = Compiler::default()
122✔
446
        .with_funs(funs)
122✔
447
        .compile(modules)
122✔
448
        .map_err(|errs| anyhow::anyhow!("JQ compile error: {:?}", errs))?;
122✔
449

450
    let jaq_input = to_jaq_val(input);
122✔
451
    let ctx = Ctx::<data::JustLut<JaqVal>>::new(&filter.lut, Vars::new([]));
122✔
452
    let mut out = filter.id.run((ctx, jaq_input)).map(unwrap_valr);
122✔
453

454
    if let Some(Ok(val)) = out.next() {
122✔
455
        Ok(from_jaq_val(&val))
122✔
456
    } else {
457
        Err(anyhow::anyhow!("JQ produced no output"))
×
458
    }
459
}
122✔
460

461
fn to_jaq_val(v: &Value) -> jaq_json::Val {
506✔
462
    use jaq_json::Num as JaqNum;
463
    match v {
506✔
464
        Value::Null => jaq_json::Val::Null,
×
465
        Value::Bool(b) => jaq_json::Val::Bool(*b),
21✔
466
        Value::Number(n) => {
87✔
467
            if let Some(i) = n.as_i64() {
87✔
468
                #[allow(clippy::cast_possible_wrap)]
469
                let isize_val = i as isize;
87✔
470
                jaq_json::Val::Num(JaqNum::Int(isize_val))
87✔
471
            } else if let Some(f) = n.as_f64() {
×
472
                jaq_json::Val::Num(JaqNum::Float(f))
×
473
            } else {
474
                jaq_json::Val::Null
×
475
            }
476
        }
477
        Value::String(s) => jaq_json::Val::utf8_str(bytes::Bytes::from(s.clone())),
216✔
478
        Value::Array(arr) => {
34✔
479
            jaq_json::Val::Arr(std::rc::Rc::new(arr.iter().map(to_jaq_val).collect()))
34✔
480
        }
481
        Value::Object(map) => {
148✔
482
            let entries: Vec<(jaq_json::Val, jaq_json::Val)> = map
148✔
483
                .iter()
148✔
484
                .map(|(k, v)| {
291✔
485
                    (
291✔
486
                        jaq_json::Val::utf8_str(bytes::Bytes::from(k.clone())),
291✔
487
                        to_jaq_val(v),
291✔
488
                    )
291✔
489
                })
291✔
490
                .collect();
148✔
491
            jaq_json::Val::Obj(std::rc::Rc::new(jaq_json::Map::from_iter(entries)))
148✔
492
        }
493
    }
494
}
506✔
495

496
fn from_jaq_val(v: &jaq_json::Val) -> Value {
141✔
497
    match v {
141✔
498
        jaq_json::Val::Null => Value::Null,
1✔
499
        jaq_json::Val::Bool(b) => Value::Bool(*b),
1✔
500
        jaq_json::Val::Num(n) => match n {
38✔
501
            jaq_json::Num::Int(i) => Value::Number(serde_json::Number::from(*i as i64)),
38✔
502
            jaq_json::Num::BigInt(_) => Value::Null,
×
503
            jaq_json::Num::Float(f) => serde_json::Number::from_f64(*f)
×
504
                .map(Value::Number)
×
505
                .unwrap_or(Value::Null),
×
506
            jaq_json::Num::Dec(f) => f
×
507
                .parse::<f64>()
×
508
                .ok()
×
509
                .and_then(serde_json::Number::from_f64)
×
510
                .map(Value::Number)
×
511
                .unwrap_or(Value::Null),
×
512
        },
513
        jaq_json::Val::BStr(b) | jaq_json::Val::TStr(b) => {
92✔
514
            String::from_utf8_lossy(b).into_owned().into()
92✔
515
        }
516
        jaq_json::Val::Arr(arr) => Value::Array(arr.iter().map(from_jaq_val).collect()),
9✔
517
        jaq_json::Val::Obj(map) => {
×
518
            let entries: serde_json::Map<String, Value> = map
×
519
                .iter()
×
520
                .map(|(k, v)| {
×
521
                    let key = match k {
×
522
                        jaq_json::Val::TStr(b) | jaq_json::Val::BStr(b) => {
×
523
                            String::from_utf8_lossy(b).into_owned()
×
524
                        }
525
                        _ => k.to_string(),
×
526
                    };
527
                    (key, from_jaq_val(v))
×
528
                })
×
529
                .collect();
×
530
            Value::Object(entries)
×
531
        }
532
    }
533
}
141✔
534

535
fn is_truthy(val: &Value) -> bool {
13✔
536
    !val.is_null() && val != &Value::Bool(false)
13✔
537
}
13✔
538

539
fn is_pass(r: &AssertionResult) -> bool {
21✔
540
    matches!(r, AssertionResult::Pass)
21✔
541
}
21✔
542

543
fn negate(r: AssertionResult) -> AssertionResult {
7✔
544
    r.negate()
7✔
545
}
7✔
546

547
fn fmt_result_short(r: &AssertionResult) -> String {
5✔
548
    match r {
5✔
NEW
549
        AssertionResult::Pass => "pass".into(),
×
550
        AssertionResult::Fail { message, .. } => message.clone(),
5✔
NEW
551
        AssertionResult::Error(e) => format!("error: {}", e),
×
552
    }
553
}
5✔
554

555
#[cfg(test)]
556
mod tests {
557
    use super::*;
558
    use serde_json::json;
559

560
    fn pm() -> PluginManager {
33✔
561
        PluginManager::new()
33✔
562
    }
33✔
563

564
    fn eval(pm: &PluginManager, expr: &str, response: &Value) -> AssertionResult {
30✔
565
        evaluate_assertion(pm, expr, response, None, None, None)
30✔
566
            .unwrap()
30✔
567
            .unwrap_or(AssertionResult::Error("AST returned None".into()))
30✔
568
    }
30✔
569

570
    #[test]
571
    fn test_equality_pass() {
1✔
572
        let r = eval(
1✔
573
            &pm(),
1✔
574
            ".status == \"success\"",
1✔
575
            &json!({"status": "success"}),
1✔
576
        );
577
        assert!(matches!(r, AssertionResult::Pass));
1✔
578
    }
1✔
579

580
    #[test]
581
    fn test_equality_fail() {
1✔
582
        let r = eval(&pm(), ".status == \"error\"", &json!({"status": "success"}));
1✔
583
        assert!(matches!(r, AssertionResult::Fail { .. }));
1✔
584
    }
1✔
585

586
    #[test]
587
    fn test_contains() {
1✔
588
        let r = eval(&pm(), ".name contains \"te\"", &json!({"name": "test"}));
1✔
589
        assert!(matches!(r, AssertionResult::Pass));
1✔
590
    }
1✔
591

592
    #[test]
593
    fn test_plugin_uuid_pass() {
1✔
594
        let r = eval(
1✔
595
            &pm(),
1✔
596
            "@uuid(.id)",
1✔
597
            &json!({"id": "550e8400-e29b-41d4-a716-446655440000"}),
1✔
598
        );
599
        assert!(matches!(r, AssertionResult::Pass));
1✔
600
    }
1✔
601

602
    #[test]
603
    fn test_plugin_uuid_fail() {
1✔
604
        let r = eval(&pm(), "@uuid(.id)", &json!({"id": "not-a-uuid"}));
1✔
605
        assert!(matches!(r, AssertionResult::Fail { .. }));
1✔
606
    }
1✔
607

608
    #[test]
609
    fn test_plugin_email_pass() {
1✔
610
        let r = eval(
1✔
611
            &pm(),
1✔
612
            "@email(.email)",
1✔
613
            &json!({"email": "test@example.com"}),
1✔
614
        );
615
        assert!(matches!(r, AssertionResult::Pass));
1✔
616
    }
1✔
617

618
    #[test]
619
    fn test_plugin_email_fail() {
1✔
620
        let r = eval(&pm(), "@email(.email)", &json!({"email": "not-an-email"}));
1✔
621
        assert!(matches!(r, AssertionResult::Fail { .. }));
1✔
622
    }
1✔
623

624
    #[test]
625
    fn test_negation_plugin_non_empty() {
1✔
626
        let r = eval(&pm(), "!@empty(.id)", &json!({"id": "user_1001"}));
1✔
627
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
628
    }
1✔
629

630
    #[test]
631
    fn test_negation_plugin_empty() {
1✔
632
        let r = eval(&pm(), "!@empty(.id)", &json!({"id": ""}));
1✔
633
        assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
1✔
634
    }
1✔
635

636
    #[test]
637
    fn test_negation_not_keyword() {
1✔
638
        let r = eval(&pm(), "not @empty(.id)", &json!({"id": "user_1001"}));
1✔
639
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
640
    }
1✔
641

642
    #[test]
643
    fn test_double_negation_bang() {
1✔
644
        let r = eval(
1✔
645
            &pm(),
1✔
646
            "!!@uuid(.id)",
1✔
647
            &json!({"id": "550e8400-e29b-41d4-a716-446655440000"}),
1✔
648
        );
649
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
650
    }
1✔
651

652
    #[test]
653
    fn test_double_negation_not_not() {
1✔
654
        let r = eval(
1✔
655
            &pm(),
1✔
656
            "not not @uuid(.id)",
1✔
657
            &json!({"id": "550e8400-e29b-41d4-a716-446655440000"}),
1✔
658
        );
659
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
660
    }
1✔
661

662
    #[test]
663
    fn test_pipe_not_non_empty() {
1✔
664
        let r = eval(&pm(), "@empty(.id) | not", &json!({"id": "user_1001"}));
1✔
665
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
666
    }
1✔
667

668
    #[test]
669
    fn test_pipe_not_empty() {
1✔
670
        let r = eval(&pm(), "@empty(.id) | not", &json!({"id": ""}));
1✔
671
        assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
1✔
672
    }
1✔
673

674
    #[test]
675
    fn test_pipe_not_not() {
1✔
676
        let r = eval(&pm(), "@empty(.id) | not not", &json!({"id": "user_1001"}));
1✔
677
        assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
1✔
678
    }
1✔
679

680
    #[test]
681
    fn test_or_first_true() {
1✔
682
        let r = eval(
1✔
683
            &pm(),
1✔
684
            "@uuid(.id) or @email(.id)",
1✔
685
            &json!({"id": "550e8400-e29b-41d4-a716-446655440000"}),
1✔
686
        );
687
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
688
    }
1✔
689

690
    #[test]
691
    fn test_or_second_true() {
1✔
692
        let r = eval(
1✔
693
            &pm(),
1✔
694
            "@uuid(.id) or @email(.id)",
1✔
695
            &json!({"id": "test@example.com"}),
1✔
696
        );
697
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
698
    }
1✔
699

700
    #[test]
701
    fn test_or_both_false() {
1✔
702
        let r = eval(
1✔
703
            &pm(),
1✔
704
            "@uuid(.id) or @email(.id)",
1✔
705
            &json!({"id": "not-valid"}),
1✔
706
        );
707
        assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
1✔
708
    }
1✔
709

710
    #[test]
711
    fn test_xor_one_true() {
1✔
712
        let r = eval(
1✔
713
            &pm(),
1✔
714
            "@uuid(.id) xor @email(.id)",
1✔
715
            &json!({"id": "550e8400-e29b-41d4-a716-446655440000"}),
1✔
716
        );
717
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
718
    }
1✔
719

720
    #[test]
721
    fn test_xor_both_true() {
1✔
722
        let r = eval(&pm(), ".x == 1 xor .y == 2", &json!({"x": 1, "y": 2}));
1✔
723
        assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
1✔
724
    }
1✔
725

726
    #[test]
727
    fn test_xor_both_false() {
1✔
728
        let r = eval(&pm(), ".x == 9 xor .y == 9", &json!({"x": 1, "y": 2}));
1✔
729
        assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
1✔
730
    }
1✔
731

732
    #[test]
733
    fn test_and_both_true() {
1✔
734
        let r = eval(
1✔
735
            &pm(),
1✔
736
            "@uuid(.id) and .id == \"550e8400-e29b-41d4-a716-446655440000\"",
1✔
737
            &json!({"id": "550e8400-e29b-41d4-a716-446655440000"}),
1✔
738
        );
739
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
740
    }
1✔
741

742
    #[test]
743
    fn test_and_left_false() {
1✔
744
        let r = eval(
1✔
745
            &pm(),
1✔
746
            "@uuid(.id) and .id == \"wrong\"",
1✔
747
            &json!({"id": "550e8400-e29b-41d4-a716-446655440000"}),
1✔
748
        );
749
        assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
1✔
750
    }
1✔
751

752
    #[test]
753
    fn test_paren_or_in_and() {
1✔
754
        let r = eval(
1✔
755
            &pm(),
1✔
756
            "(@uuid(.a) or @email(.b)) and .c == 1",
1✔
757
            &json!({"a": "550e8400-e29b-41d4-a716-446655440000", "b": "x", "c": 1}),
1✔
758
        );
759
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
760
    }
1✔
761

762
    #[test]
763
    fn test_negated_paren_or() {
1✔
764
        let r = eval(
1✔
765
            &pm(),
1✔
766
            "!(@uuid(.id) or @email(.id))",
1✔
767
            &json!({"id": "550e8400-e29b-41d4-a716-446655440000"}),
1✔
768
        );
769
        assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
1✔
770
    }
1✔
771

772
    #[test]
773
    fn test_negated_paren_or_both_false() {
1✔
774
        let r = eval(
1✔
775
            &pm(),
1✔
776
            "!(@uuid(.id) or @email(.id))",
1✔
777
            &json!({"id": "garbage"}),
1✔
778
        );
779
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
780
    }
1✔
781

782
    #[test]
783
    fn test_has_header_with_headers() {
1✔
784
        let p = pm();
1✔
785
        let mut headers = HashMap::new();
1✔
786
        headers.insert("content-type".to_string(), "application/json".to_string());
1✔
787
        let r = evaluate_assertion(
1✔
788
            &p,
1✔
789
            "@has_header(\"content-type\") == true",
1✔
790
            &json!({}),
1✔
791
            Some(&headers),
1✔
792
            None,
1✔
793
            None,
1✔
794
        )
795
        .unwrap()
1✔
796
        .unwrap();
1✔
797
        assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1✔
798
    }
1✔
799

800
    #[test]
801
    fn test_trailer_value_plugin() {
1✔
802
        let p = pm();
1✔
803
        let mut trailers = HashMap::new();
1✔
804
        trailers.insert("grpc-status".to_string(), "0".to_string());
1✔
805
        let r = evaluate_assertion(
1✔
806
            &p,
1✔
807
            "@trailer(\"grpc-status\") == \"0\"",
1✔
808
            &json!({}),
1✔
809
            None,
1✔
810
            Some(&trailers),
1✔
811
            None,
1✔
812
        )
813
        .unwrap()
1✔
814
        .unwrap();
1✔
815
        assert!(matches!(r, AssertionResult::Pass));
1✔
816
    }
1✔
817

818
    #[test]
819
    fn test_numeric_greater() {
1✔
820
        let r = eval(&pm(), ".id > 100", &json!({"id": 123}));
1✔
821
        assert!(matches!(r, AssertionResult::Pass));
1✔
822
    }
1✔
823

824
    #[test]
825
    fn test_numeric_less() {
1✔
826
        let r = eval(&pm(), ".id < 200", &json!({"id": 123}));
1✔
827
        assert!(matches!(r, AssertionResult::Pass));
1✔
828
    }
1✔
829

830
    #[test]
831
    fn test_matches_regex() {
1✔
832
        let r = eval(&pm(), ".name matches \"^te.*t$\"", &json!({"name": "test"}));
1✔
833
        assert!(matches!(r, AssertionResult::Pass));
1✔
834
    }
1✔
835

836
    #[test]
837
    fn test_matches_regex_fail() {
1✔
838
        let r = eval(&pm(), ".name matches \"^xyz\"", &json!({"name": "test"}));
1✔
839
        assert!(matches!(r, AssertionResult::Fail { .. }));
1✔
840
    }
1✔
841

842
    #[test]
843
    fn test_jq_fallback_via_raw() {
1✔
844
        let p = pm();
1✔
845
        let r = evaluate_assertion(
1✔
846
            &p,
1✔
847
            ".tags | length",
1✔
848
            &json!({"tags": [1, 2, 3]}),
1✔
849
            None,
1✔
850
            None,
1✔
851
            None,
1✔
852
        )
853
        .unwrap();
1✔
854
        assert!(
1✔
855
            r.is_none(),
1✔
856
            "JQ pipe should return None to trigger JQ fallback"
857
        );
858
    }
1✔
859

860
    #[test]
861
    fn test_resolve_path_simple() {
1✔
862
        let r = resolve_path(".key", &json!({"key": "value"}));
1✔
863
        assert_eq!(r, json!("value"));
1✔
864
    }
1✔
865

866
    #[test]
867
    fn test_resolve_path_nested() {
1✔
868
        let r = resolve_path(".outer.inner", &json!({"outer": {"inner": "value"}}));
1✔
869
        assert_eq!(r, json!("value"));
1✔
870
    }
1✔
871

872
    #[test]
873
    fn test_resolve_path_array_index() {
1✔
874
        let r = resolve_path(".items[0]", &json!({"items": ["first", "second"]}));
1✔
875
        assert_eq!(r, json!("first"));
1✔
876
    }
1✔
877

878
    #[test]
879
    fn test_resolve_path_missing_key() {
1✔
880
        let r = resolve_path(".missing", &json!({"a": 1}));
1✔
881
        assert!(r.is_null());
1✔
882
    }
1✔
883

884
    #[test]
885
    fn test_compare_numeric_greater() {
1✔
886
        assert_eq!(compare_numeric(&json!(5), &json!(3), ">"), Some(true));
1✔
887
    }
1✔
888

889
    #[test]
890
    fn test_compare_numeric_less() {
1✔
891
        assert_eq!(compare_numeric(&json!(3), &json!(5), "<"), Some(true));
1✔
892
    }
1✔
893

894
    #[test]
895
    fn test_compare_numeric_equality() {
1✔
896
        assert_eq!(compare_numeric(&json!(5), &json!(5), ">="), Some(true));
1✔
897
        assert_eq!(compare_numeric(&json!(5), &json!(5), "<="), Some(true));
1✔
898
    }
1✔
899

900
    #[test]
901
    fn test_compare_numeric_mixed_types() {
1✔
902
        assert_eq!(compare_numeric(&json!(5), &json!("5"), ">"), None);
1✔
903
    }
1✔
904

905
    #[test]
906
    fn test_cached_regex_valid() {
1✔
907
        assert!(cached_regex(r"\d+").is_ok());
1✔
908
    }
1✔
909

910
    #[test]
911
    fn test_cached_regex_invalid() {
1✔
912
        assert!(cached_regex(r"[").is_err());
1✔
913
    }
1✔
914
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc