Skip to main content

alef_e2e/codegen/rust/
assertions.rs

1//! Assertion rendering for Rust e2e tests.
2
3use std::fmt::Write as FmtWrite;
4
5use crate::escape::escape_rust;
6use crate::field_access::FieldResolver;
7use crate::fixture::Assertion;
8
9use super::assertion_helpers::{
10    render_count_equals_assertion, render_count_min_assertion, render_equals_assertion, render_gte_assertion,
11    render_is_empty_assertion, render_method_result_assertion, render_not_empty_assertion,
12};
13use super::assertion_synthetic::{
14    numeric_literal, render_chunks_have_content, render_chunks_have_embeddings, render_chunks_have_heading_context,
15    render_embedding_dimensions, render_embedding_quality, render_embeddings_assertion,
16    render_first_chunk_starts_with_heading, render_keywords_assertion, render_keywords_count_assertion,
17    tree_field_access_expr, value_to_rust_string,
18};
19
20/// Returns `true` when the assertion's leaf field resolves to an `Option<T>` where
21/// `T` is a scalar (i.e. not a collection). Used to decide whether numeric comparison
22/// operators (`>`, `<`, `>=`, `<=`) need to unwrap the field before comparing — directly
23/// comparing `Option<usize>` against a numeric literal is a type error.
24fn is_optional_scalar_field(assertion: &Assertion, is_unwrapped: bool, field_resolver: &FieldResolver) -> bool {
25    assertion.field.as_ref().is_some_and(|f| {
26        let resolved = field_resolver.resolve(f);
27        let is_opt = !is_unwrapped && field_resolver.is_optional(resolved);
28        let is_arr = field_resolver.is_array(resolved);
29        is_opt && !is_arr
30    })
31}
32
33/// Render a single assertion into the test function body.
34#[allow(clippy::too_many_arguments)]
35pub fn render_assertion(
36    out: &mut String,
37    assertion: &Assertion,
38    result_var: &str,
39    module: &str,
40    dep_name: &str,
41    is_error_context: bool,
42    unwrapped_fields: &[(String, String)], // (fixture_field, local_var)
43    field_resolver: &FieldResolver,
44    result_is_tree: bool,
45    result_is_simple: bool,
46    result_is_vec: bool,
47    result_is_option: bool,
48    returns_result: bool,
49) {
50    render_assertion_with_streaming(
51        out,
52        assertion,
53        result_var,
54        module,
55        dep_name,
56        is_error_context,
57        unwrapped_fields,
58        field_resolver,
59        result_is_tree,
60        result_is_simple,
61        result_is_vec,
62        result_is_option,
63        returns_result,
64        false,
65    )
66}
67
68/// Same as [`render_assertion`], but with an `is_streaming` flag so the streaming-virtual
69/// field arm can fire when `result_var` is the raw call result rather than the collected
70/// `chunks` variable.  Callers that already drained the stream into a `chunks: Vec<_>`
71/// local should pass `is_streaming = true`.
72#[allow(clippy::too_many_arguments)]
73pub fn render_assertion_with_streaming(
74    out: &mut String,
75    assertion: &Assertion,
76    result_var: &str,
77    module: &str,
78    dep_name: &str,
79    is_error_context: bool,
80    unwrapped_fields: &[(String, String)], // (fixture_field, local_var)
81    field_resolver: &FieldResolver,
82    result_is_tree: bool,
83    result_is_simple: bool,
84    result_is_vec: bool,
85    result_is_option: bool,
86    returns_result: bool,
87    _is_streaming: bool,
88) {
89    // Vec<T> result: iterate per-element so each assertion checks every element.
90    // Field-path assertions become `for r in &{result} { <assert using r> }`.
91    // Length-style assertions on the Vec itself (no field path) operate on the
92    // Vec directly.
93    let has_field = assertion.field.as_ref().is_some_and(|f| !f.is_empty());
94    if result_is_vec && has_field && !is_error_context {
95        let _ = writeln!(out, "    for r in &{result_var} {{");
96        render_assertion(
97            out,
98            assertion,
99            "r",
100            module,
101            dep_name,
102            is_error_context,
103            unwrapped_fields,
104            field_resolver,
105            result_is_tree,
106            result_is_simple,
107            false, // already inside loop
108            result_is_option,
109            returns_result,
110        );
111        let _ = writeln!(out, "    }}");
112        return;
113    }
114    // Option<T> result: map `is_empty`/`not_empty` to `is_none()`/`is_some()`,
115    // and unwrap the inner value before any other assertion runs.
116    if result_is_option && !is_error_context {
117        let assertion_type = assertion.assertion_type.as_str();
118        if !has_field && (assertion_type == "is_empty" || assertion_type == "not_empty") {
119            let check = if assertion_type == "is_empty" {
120                "is_none"
121            } else {
122                "is_some"
123            };
124            let _ = writeln!(
125                out,
126                "    assert!({result_var}.{check}(), \"expected Option to be {check}\");"
127            );
128            return;
129        }
130        // For any other assertion shape, unwrap the Option and recurse with a
131        // bare reference variable so the rest of the renderer treats the inner
132        // value as the result.
133        let _ = writeln!(
134            out,
135            "    let r = {result_var}.as_ref().expect(\"Option<T> should be Some\");"
136        );
137        render_assertion(
138            out,
139            assertion,
140            "r",
141            module,
142            dep_name,
143            is_error_context,
144            unwrapped_fields,
145            field_resolver,
146            result_is_tree,
147            result_is_simple,
148            result_is_vec,
149            false, // already unwrapped
150            returns_result,
151        );
152        return;
153    }
154    // Handle synthetic fields like chunks_have_content (derived assertions).
155    // These are computed expressions, not real struct fields — intercept before
156    // the is_valid_for_result check so they are never treated as field accesses.
157    if let Some(f) = &assertion.field {
158        match f.as_str() {
159            "chunks_have_content" => {
160                render_chunks_have_content(out, result_var, assertion.assertion_type.as_str());
161                return;
162            }
163            "chunks_have_embeddings" => {
164                render_chunks_have_embeddings(out, result_var, assertion.assertion_type.as_str());
165                return;
166            }
167            "chunks_have_heading_context" => {
168                render_chunks_have_heading_context(out, result_var, assertion.assertion_type.as_str());
169                return;
170            }
171            "first_chunk_starts_with_heading" => {
172                render_first_chunk_starts_with_heading(out, result_var, assertion.assertion_type.as_str());
173                return;
174            }
175            "embeddings" => {
176                render_embeddings_assertion(out, result_var, assertion);
177                return;
178            }
179            "embedding_dimensions" => {
180                render_embedding_dimensions(out, result_var, assertion);
181                return;
182            }
183            "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
184                render_embedding_quality(out, result_var, f, assertion.assertion_type.as_str());
185                return;
186            }
187            "keywords" => {
188                render_keywords_assertion(out, result_var, assertion);
189                return;
190            }
191            "keywords_count" => {
192                render_keywords_count_assertion(out, result_var, assertion);
193                return;
194            }
195            _ => {}
196        }
197    }
198
199    // Streaming virtual fields: intercept before is_valid_for_result so they are
200    // never skipped.  These fields resolve against the `chunks` collected-list variable.
201    //
202    // For streaming fixtures, `chunks` is bound by the collect snippet emitted in
203    // `render_test_function`.  For non-streaming fixtures whose result struct has a
204    // literal field whose name collides with a streaming-virtual name (e.g. `chunks`,
205    // `imports`, `structure`), `render_test_function` emits `let {f} = &result.{f};`
206    // before assertions, so the hardcoded `chunks` identifier used below still resolves.
207    if let Some(f) = &assertion.field {
208        if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
209            if let Some(expr) =
210                crate::codegen::streaming_assertions::StreamingFieldResolver::accessor_with_module_qualifier(
211                    f,
212                    "rust",
213                    "chunks",
214                    Some(dep_name),
215                )
216            {
217                match assertion.assertion_type.as_str() {
218                    "count_min" => {
219                        if let Some(val) = &assertion.value {
220                            if let Some(n) = val.as_u64() {
221                                let expr_for_len = if field_resolver.is_optional(f) {
222                                    format!("{expr}.as_ref().map_or(0, |v| v.len())")
223                                } else {
224                                    format!("{expr}.len()")
225                                };
226                                let _ = writeln!(
227                                    out,
228                                    "    assert!({expr_for_len} >= {n} as usize, \"expected >= {n} chunks\");"
229                                );
230                            }
231                        }
232                    }
233                    "count_equals" => {
234                        if let Some(val) = &assertion.value {
235                            if let Some(n) = val.as_u64() {
236                                let expr_for_len = if field_resolver.is_optional(f) {
237                                    format!("{expr}.as_ref().map_or(0, |v| v.len())")
238                                } else {
239                                    format!("{expr}.len()")
240                                };
241                                let _ = writeln!(
242                                    out,
243                                    "    assert_eq!({expr_for_len}, {n} as usize, \"expected exactly {n} chunks\");"
244                                );
245                            }
246                        }
247                    }
248                    "equals" => {
249                        if let Some(serde_json::Value::String(s)) = &assertion.value {
250                            let escaped = crate::escape::escape_rust(s);
251                            let _ = writeln!(out, "    assert_eq!({expr}, \"{escaped}\");");
252                        } else if let Some(val) = &assertion.value {
253                            let lit = super::assertion_synthetic::numeric_literal(val);
254                            let _ = writeln!(out, "    assert_eq!({expr}, {lit});");
255                        }
256                    }
257                    "not_empty" => {
258                        let check_expr = if field_resolver.is_optional(f) {
259                            format!("{expr}.as_ref().is_some_and(|v| !v.is_empty())")
260                        } else {
261                            format!("!{expr}.is_empty()")
262                        };
263                        let _ = writeln!(out, "    assert!({check_expr}, \"expected non-empty\");");
264                    }
265                    "is_empty" => {
266                        let check_expr = if field_resolver.is_optional(f) {
267                            format!("{expr}.as_ref().is_none_or(|v| v.is_empty())")
268                        } else {
269                            format!("{expr}.is_empty()")
270                        };
271                        let _ = writeln!(out, "    assert!({check_expr}, \"expected empty\");");
272                    }
273                    "is_true" => {
274                        let _ = writeln!(out, "    assert!({expr}, \"expected true\");");
275                    }
276                    "is_false" => {
277                        let _ = writeln!(out, "    assert!(!{expr}, \"expected false\");");
278                    }
279                    "greater_than" => {
280                        if let Some(val) = &assertion.value {
281                            let lit = super::assertion_synthetic::numeric_literal(val);
282                            let _ = writeln!(out, "    assert!({expr} > {lit}, \"expected > {lit}\");");
283                        }
284                    }
285                    "greater_than_or_equal" => {
286                        if let Some(val) = &assertion.value {
287                            let lit = super::assertion_synthetic::numeric_literal(val);
288                            let _ = writeln!(out, "    assert!({expr} >= {lit}, \"expected >= {lit}\");");
289                        }
290                    }
291                    "contains" => {
292                        if let Some(serde_json::Value::String(s)) = &assertion.value {
293                            let escaped = crate::escape::escape_rust(s);
294                            let _ = writeln!(
295                                out,
296                                "    assert!({expr}.contains(\"{escaped}\"), \"expected to contain: {escaped}\");"
297                            );
298                        }
299                    }
300                    _ => {
301                        let _ = writeln!(
302                            out,
303                            "    // streaming field '{f}': assertion type '{}' not rendered",
304                            assertion.assertion_type
305                        );
306                    }
307                }
308            }
309            return;
310        }
311    }
312
313    // Skip assertions on fields that don't exist on the result type.
314    // Exception: fields prefixed with "error." target the error value in error-context
315    // assertions — they are resolved against the error type via accessor_for_error,
316    // not against the success result type, so they must not be skipped here.
317    // However, when NOT in error context (i.e. the call site uses .expect() and binds
318    // the Ok value), there is no Err to inspect — skip error.* assertions with a comment.
319    if let Some(f) = &assertion.field {
320        if !f.is_empty() {
321            if f.starts_with("error.") && !is_error_context {
322                let _ = writeln!(out, "    // skipped: field '{f}' not available on result type");
323                return;
324            }
325            if !f.starts_with("error.") && !field_resolver.is_valid_for_result(f) {
326                let _ = writeln!(out, "    // skipped: field '{f}' not available on result type");
327                return;
328            }
329        }
330    }
331
332    // Check if this field was unwrapped (i.e., it is optional and was bound to a local).
333    let is_unwrapped = assertion
334        .field
335        .as_ref()
336        .is_some_and(|f| unwrapped_fields.iter().any(|(ff, _)| ff == f));
337
338    // When in error context with returns_result=true and accessing a field (not an error check),
339    // we need to unwrap the Result first. The test generator creates a binding like
340    // `let result_ok = result.as_ref().ok();` which we can dereference here.
341    // Exception: fields prefixed with "error." access the Err value, not the Ok value.
342    let has_field = assertion.field.as_ref().is_some_and(|f| !f.is_empty());
343    let is_field_assertion = !matches!(assertion.assertion_type.as_str(), "error" | "not_error");
344    let is_error_field = assertion.field.as_ref().is_some_and(|f| f.starts_with("error."));
345    let effective_result_var =
346        if has_field && is_error_context && returns_result && is_field_assertion && !is_error_field {
347            // Dereference the Option<&T> bound as {result_var}_ok
348            format!("{result_var}_ok.as_ref().unwrap()")
349        } else {
350            result_var.to_string()
351        };
352
353    // Determine field access expression:
354    // 1. If the field was unwrapped to a local var, use that local var name.
355    // 2. When result_is_simple, the function returns a plain type (String etc.) — use result_var.
356    // 3. When the field path is exactly the result var name (sentinel: `field: "result"`),
357    //    refer to the result variable directly to avoid emitting `result.result`.
358    // 4. When the result is a Tree, map pseudo-field names to correct Rust expressions.
359    // 5. When the field starts with "error.", resolve against the error type.
360    // 6. Otherwise, use the field resolver to generate the accessor.
361    let field_access = match &assertion.field {
362        Some(f) if !f.is_empty() => {
363            if let Some((_, local_var)) = unwrapped_fields.iter().find(|(ff, _)| ff == f) {
364                local_var.clone()
365            } else if result_is_simple && !f.starts_with("error.") {
366                // Plain return type (String, Vec<T>, etc.) has no struct fields.
367                // Use the result variable directly so assertions operate on the value itself.
368                // Exception: error.* fields must resolve against the Err value, not the
369                // plain result variable, even when the success type is simple (e.g. Bytes).
370                effective_result_var.clone()
371            } else if f == result_var {
372                // Sentinel: fixture uses `field: "result"` (or matches the result variable name)
373                // to refer to the whole return value, not a struct field named "result".
374                effective_result_var.clone()
375            } else if result_is_tree {
376                // Tree is an opaque type — its "fields" are accessed via root_node() or
377                // free functions. Map known pseudo-field names to correct Rust expressions.
378                tree_field_access_expr(f, &effective_result_var, module)
379            } else if let Some(sub) = f.strip_prefix("error.") {
380                // Error-path field: access a field on the Err value rather than the Ok value.
381                // Inline-bind the error so the expression is self-contained.
382                let err_accessor = field_resolver.accessor_for_error(sub, "rust", "__err");
383                format!("{{ let __err = {result_var}.as_ref().err().unwrap(); {err_accessor} }}")
384            } else {
385                field_resolver.accessor(f, "rust", &effective_result_var)
386            }
387        }
388        _ => effective_result_var,
389    };
390
391    match assertion.assertion_type.as_str() {
392        "error" => {
393            let _ = writeln!(out, "    assert!({result_var}.is_err(), \"expected call to fail\");");
394            if let Some(serde_json::Value::String(msg)) = &assertion.value {
395                let escaped = escape_rust(msg);
396                // Match against the Debug format (variant-name-style) and the Display format
397                // (human-readable text). Fixtures often name the error variant ("BadRequest"),
398                // but Display impls typically lowercase with a colon ("bad request: ..."), so
399                // checking both lets either kind of fixture value match.
400                let _ = writeln!(
401                    out,
402                    "    {{ let __e = {result_var}.as_ref().err().unwrap(); assert!(format!(\"{{:?}}\", __e).contains(\"{escaped}\") || __e.to_string().contains(\"{escaped}\"), \"error message mismatch\"); }}"
403                );
404            }
405        }
406        "not_error" => {
407            // Handled at call site; nothing extra needed here.
408        }
409        "equals" => {
410            render_equals_assertion(out, assertion, &field_access, is_unwrapped, field_resolver);
411        }
412        "contains" => {
413            if let Some(val) = &assertion.value {
414                let expected = value_to_rust_string(val);
415                let line = format!(
416                    "    assert!(format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected to contain: {{}}\", {expected});"
417                );
418                let _ = writeln!(out, "{line}");
419            }
420        }
421        "contains_all" => {
422            if let Some(values) = &assertion.values {
423                for val in values {
424                    let expected = value_to_rust_string(val);
425                    let line = format!(
426                        "    assert!(format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected to contain: {{}}\", {expected});"
427                    );
428                    let _ = writeln!(out, "{line}");
429                }
430            }
431        }
432        "not_contains" => {
433            if let Some(val) = &assertion.value {
434                let expected = value_to_rust_string(val);
435                let line = format!(
436                    "    assert!(!format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected NOT to contain: {{}}\", {expected});"
437                );
438                let _ = writeln!(out, "{line}");
439            }
440        }
441        "not_empty" => {
442            render_not_empty_assertion(
443                out,
444                assertion,
445                &field_access,
446                result_var,
447                result_is_option,
448                is_unwrapped,
449                field_resolver,
450            );
451        }
452        "is_empty" => {
453            render_is_empty_assertion(out, assertion, &field_access, is_unwrapped, field_resolver);
454        }
455        "contains_any" => {
456            if let Some(values) = &assertion.values {
457                let checks: Vec<String> = values
458                    .iter()
459                    .map(|v| {
460                        let expected = value_to_rust_string(v);
461                        format!("{field_access}.contains({expected})")
462                    })
463                    .collect();
464                let joined = checks.join(" || ");
465                let _ = writeln!(
466                    out,
467                    "    assert!({joined}, \"expected to contain at least one of the specified values\");"
468                );
469            }
470        }
471        "greater_than" => {
472            if let Some(val) = &assertion.value {
473                // Skip comparisons with negative values against unsigned types (.len() etc.)
474                if val.as_f64().is_some_and(|n| n < 0.0) {
475                    let _ = writeln!(
476                        out,
477                        "    // skipped: greater_than with negative value is always true for unsigned types"
478                    );
479                } else if val.as_u64() == Some(0) {
480                    if field_access.ends_with(".len()") {
481                        // Clippy prefers !is_empty() over len() > 0 for collections.
482                        let base = field_access.strip_suffix(".len()").unwrap();
483                        let _ = writeln!(out, "    assert!(!{base}.is_empty(), \"expected > 0\");");
484                    } else if is_optional_scalar_field(assertion, is_unwrapped, field_resolver) {
485                        // Use 0 for integer comparisons (the common case for > 0).
486                        let _ = writeln!(out, "    assert!({field_access}.unwrap_or(0) > 0, \"expected > 0\");");
487                    } else {
488                        // Scalar types (usize, u64, etc.) — use direct comparison.
489                        let _ = writeln!(out, "    assert!({field_access} > 0, \"expected > 0\");");
490                    }
491                } else {
492                    let lit = numeric_literal(val);
493                    if is_optional_scalar_field(assertion, is_unwrapped, field_resolver) {
494                        // Option<usize>/Option<u64>/Option<f64>: unwrap with appropriate zero literal
495                        // before comparing so the assertion fails (rather than fails to compile) on a missing field.
496                        let default_literal = if lit.contains("_f64") || lit.contains('.') {
497                            "0.0"
498                        } else {
499                            "0"
500                        };
501                        let _ = writeln!(
502                            out,
503                            "    assert!({field_access}.unwrap_or({default_literal}) > {lit}, \"expected > {lit}\");"
504                        );
505                    } else {
506                        let _ = writeln!(out, "    assert!({field_access} > {lit}, \"expected > {lit}\");");
507                    }
508                }
509            }
510        }
511        "less_than" => {
512            if let Some(val) = &assertion.value {
513                let lit = numeric_literal(val);
514                if is_optional_scalar_field(assertion, is_unwrapped, field_resolver) {
515                    // Option<usize>/Option<u64>/Option<f64>: unwrap with appropriate zero literal
516                    // before comparing. Note this means a missing field will satisfy `< N` for any positive N,
517                    // matching the convention used by render_gte_assertion.
518                    let default_literal = if lit.contains("_f64") || lit.contains('.') {
519                        "0.0"
520                    } else {
521                        "0"
522                    };
523                    let _ = writeln!(
524                        out,
525                        "    assert!({field_access}.unwrap_or({default_literal}) < {lit}, \"expected < {lit}\");"
526                    );
527                } else {
528                    let _ = writeln!(out, "    assert!({field_access} < {lit}, \"expected < {lit}\");");
529                }
530            }
531        }
532        "greater_than_or_equal" => {
533            render_gte_assertion(out, assertion, &field_access, is_unwrapped, field_resolver);
534        }
535        "less_than_or_equal" => {
536            if let Some(val) = &assertion.value {
537                let lit = numeric_literal(val);
538                if is_optional_scalar_field(assertion, is_unwrapped, field_resolver) {
539                    // Option<usize>/Option<u64>/Option<f64>: unwrap with appropriate zero literal.
540                    let default_literal = if lit.contains("_f64") || lit.contains('.') {
541                        "0.0"
542                    } else {
543                        "0"
544                    };
545                    let _ = writeln!(
546                        out,
547                        "    assert!({field_access}.unwrap_or({default_literal}) <= {lit}, \"expected <= {lit}\");"
548                    );
549                } else {
550                    let _ = writeln!(out, "    assert!({field_access} <= {lit}, \"expected <= {lit}\");");
551                }
552            }
553        }
554        "starts_with" => {
555            if let Some(val) = &assertion.value {
556                let expected = value_to_rust_string(val);
557                let _ = writeln!(
558                    out,
559                    "    assert!({field_access}.starts_with({expected}), \"expected to start with: {{}}\", {expected});"
560                );
561            }
562        }
563        "ends_with" => {
564            if let Some(val) = &assertion.value {
565                let expected = value_to_rust_string(val);
566                let _ = writeln!(
567                    out,
568                    "    assert!({field_access}.ends_with({expected}), \"expected to end with: {{}}\", {expected});"
569                );
570            }
571        }
572        "min_length" => {
573            if let Some(val) = &assertion.value {
574                if let Some(n) = val.as_u64() {
575                    let _ = writeln!(
576                        out,
577                        "    assert!({field_access}.len() >= {n}, \"expected length >= {n}, got {{}}\", {field_access}.len());"
578                    );
579                }
580            }
581        }
582        "max_length" => {
583            if let Some(val) = &assertion.value {
584                if let Some(n) = val.as_u64() {
585                    let _ = writeln!(
586                        out,
587                        "    assert!({field_access}.len() <= {n}, \"expected length <= {n}, got {{}}\", {field_access}.len());"
588                    );
589                }
590            }
591        }
592        "count_min" => {
593            render_count_min_assertion(out, assertion, &field_access, is_unwrapped, field_resolver);
594        }
595        "count_equals" => {
596            render_count_equals_assertion(out, assertion, &field_access, is_unwrapped, field_resolver);
597        }
598        "is_true" => {
599            if is_optional_scalar_field(assertion, is_unwrapped, field_resolver) {
600                // Option<T>: "is_true" semantically means "present and truthy".
601                // For `Option<bool>` that's `Some(true)`; for `Option<serde_json::Value>`
602                // (e.g. interact action_results[0].data) it's "Some and not null/false".
603                // `is_some()` is the broadest correct interpretation that compiles for any T.
604                let _ = writeln!(out, "    assert!({field_access}.is_some(), \"expected true (Some)\");");
605            } else {
606                let _ = writeln!(out, "    assert!({field_access}, \"expected true\");");
607            }
608        }
609        "is_false" => {
610            if is_optional_scalar_field(assertion, is_unwrapped, field_resolver) {
611                // Option<T>: "is_false" semantically means "absent or falsy" — `.is_none()`
612                // is the safe interpretation that compiles uniformly.
613                let _ = writeln!(out, "    assert!({field_access}.is_none(), \"expected false (None)\");");
614            } else {
615                let _ = writeln!(out, "    assert!(!{field_access}, \"expected false\");");
616            }
617        }
618        "method_result" => {
619            render_method_result_assertion(out, assertion, &field_access, result_is_tree, module);
620        }
621        other => {
622            panic!("Rust e2e generator: unsupported assertion type: {other}");
623        }
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use std::collections::{HashMap, HashSet};
630
631    use super::*;
632    use crate::field_access::FieldResolver;
633    use crate::fixture::Assertion;
634
635    fn empty_resolver() -> FieldResolver {
636        FieldResolver::new(
637            &HashMap::new(),
638            &HashSet::new(),
639            &HashSet::new(),
640            &HashSet::new(),
641            &HashSet::new(),
642        )
643    }
644
645    fn make_assertion(assertion_type: &str, field: Option<&str>, value: Option<serde_json::Value>) -> Assertion {
646        Assertion {
647            assertion_type: assertion_type.to_string(),
648            field: field.map(|s| s.to_string()),
649            value,
650            ..Default::default()
651        }
652    }
653
654    #[test]
655    fn render_assertion_error_type_emits_is_err_check() {
656        let resolver = empty_resolver();
657        let assertion = make_assertion("error", None, None);
658        let mut out = String::new();
659        render_assertion(
660            &mut out,
661            &assertion,
662            "result",
663            "my_mod",
664            "dep",
665            true,
666            &[],
667            &resolver,
668            false,
669            false,
670            false,
671            false,
672            false,
673        );
674        assert!(out.contains("is_err()"), "got: {out}");
675    }
676
677    #[test]
678    fn render_assertion_vec_result_wraps_in_for_loop() {
679        let resolver = empty_resolver();
680        let assertion = make_assertion("not_empty", Some("content"), None);
681        let mut out = String::new();
682        render_assertion(
683            &mut out,
684            &assertion,
685            "result",
686            "my_mod",
687            "dep",
688            false,
689            &[],
690            &resolver,
691            false,
692            false,
693            true,
694            false,
695            false,
696        );
697        assert!(out.contains("for r in"), "got: {out}");
698    }
699
700    #[test]
701    fn render_assertion_not_empty_bare_result_uses_is_empty() {
702        let resolver = empty_resolver();
703        let assertion = make_assertion("not_empty", None, None);
704        let mut out = String::new();
705        render_assertion(
706            &mut out,
707            &assertion,
708            "result",
709            "my_mod",
710            "dep",
711            false,
712            &[],
713            &resolver,
714            false,
715            false,
716            false,
717            false,
718            false,
719        );
720        assert!(out.contains("is_empty()"), "got: {out}");
721    }
722}