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 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 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 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 let is_unwrapped = assertion
334 .field
335 .as_ref()
336 .is_some_and(|f| unwrapped_fields.iter().any(|(ff, _)| ff == f));
337
338 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 format!("{result_var}_ok.as_ref().unwrap()")
349 } else {
350 result_var.to_string()
351 };
352
353 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 effective_result_var.clone()
371 } else if f == result_var {
372 effective_result_var.clone()
375 } else if result_is_tree {
376 tree_field_access_expr(f, &effective_result_var, module)
379 } else if let Some(sub) = f.strip_prefix("error.") {
380 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 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 }
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 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 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 let _ = writeln!(out, " assert!({field_access}.unwrap_or(0) > 0, \"expected > 0\");");
487 } else {
488 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 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 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 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 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 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}