1use 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
20fn 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#[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)], 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#[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)], 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 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, result_is_option,
109 returns_result,
110 );
111 let _ = writeln!(out, " }}");
112 return;
113 }
114 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 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, returns_result,
151 );
152 return;
153 }
154 let _ = dep_name;
155 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 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 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 let is_unwrapped = assertion
330 .field
331 .as_ref()
332 .is_some_and(|f| unwrapped_fields.iter().any(|(ff, _)| ff == f));
333
334 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 format!("{result_var}_ok.as_ref().unwrap()")
345 } else {
346 result_var.to_string()
347 };
348
349 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 effective_result_var.clone()
367 } else if f == result_var {
368 effective_result_var.clone()
371 } else if result_is_tree {
372 tree_field_access_expr(f, &effective_result_var, module)
375 } else if let Some(sub) = f.strip_prefix("error.") {
376 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 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 }
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 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 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 let _ = writeln!(out, " assert!({field_access}.unwrap_or(0) > 0, \"expected > 0\");");
483 } else {
484 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 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 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 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 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 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}