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