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_embedding_dimensions,
15 render_embedding_quality, render_embeddings_assertion, render_keywords_assertion, render_keywords_count_assertion,
16 tree_field_access_expr, value_to_rust_string,
17};
18
19#[allow(clippy::too_many_arguments)]
21pub fn render_assertion(
22 out: &mut String,
23 assertion: &Assertion,
24 result_var: &str,
25 module: &str,
26 dep_name: &str,
27 is_error_context: bool,
28 unwrapped_fields: &[(String, String)], field_resolver: &FieldResolver,
30 result_is_tree: bool,
31 result_is_simple: bool,
32 result_is_vec: bool,
33 result_is_option: bool,
34 returns_result: bool,
35) {
36 let has_field = assertion.field.as_ref().is_some_and(|f| !f.is_empty());
41 if result_is_vec && has_field && !is_error_context {
42 let _ = writeln!(out, " for r in &{result_var} {{");
43 render_assertion(
44 out,
45 assertion,
46 "r",
47 module,
48 dep_name,
49 is_error_context,
50 unwrapped_fields,
51 field_resolver,
52 result_is_tree,
53 result_is_simple,
54 false, result_is_option,
56 returns_result,
57 );
58 let _ = writeln!(out, " }}");
59 return;
60 }
61 if result_is_option && !is_error_context {
64 let assertion_type = assertion.assertion_type.as_str();
65 if !has_field && (assertion_type == "is_empty" || assertion_type == "not_empty") {
66 let check = if assertion_type == "is_empty" {
67 "is_none"
68 } else {
69 "is_some"
70 };
71 let _ = writeln!(
72 out,
73 " assert!({result_var}.{check}(), \"expected Option to be {check}\");"
74 );
75 return;
76 }
77 let _ = writeln!(
81 out,
82 " let r = {result_var}.as_ref().expect(\"Option<T> should be Some\");"
83 );
84 render_assertion(
85 out,
86 assertion,
87 "r",
88 module,
89 dep_name,
90 is_error_context,
91 unwrapped_fields,
92 field_resolver,
93 result_is_tree,
94 result_is_simple,
95 result_is_vec,
96 false, returns_result,
98 );
99 return;
100 }
101 let _ = dep_name;
102 if let Some(f) = &assertion.field {
106 match f.as_str() {
107 "chunks_have_content" => {
108 render_chunks_have_content(out, result_var, assertion.assertion_type.as_str());
109 return;
110 }
111 "chunks_have_embeddings" => {
112 render_chunks_have_embeddings(out, result_var, assertion.assertion_type.as_str());
113 return;
114 }
115 "embeddings" => {
116 render_embeddings_assertion(out, result_var, assertion);
117 return;
118 }
119 "embedding_dimensions" => {
120 render_embedding_dimensions(out, result_var, assertion);
121 return;
122 }
123 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
124 render_embedding_quality(out, result_var, f, assertion.assertion_type.as_str());
125 return;
126 }
127 "keywords" => {
128 render_keywords_assertion(out, result_var, assertion);
129 return;
130 }
131 "keywords_count" => {
132 render_keywords_count_assertion(out, result_var, assertion);
133 return;
134 }
135 _ => {}
136 }
137 }
138
139 if let Some(f) = &assertion.field {
141 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
142 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
143 return;
144 }
145 }
146
147 let is_unwrapped = assertion
149 .field
150 .as_ref()
151 .is_some_and(|f| unwrapped_fields.iter().any(|(ff, _)| ff == f));
152
153 let has_field = assertion.field.as_ref().is_some_and(|f| !f.is_empty());
157 let is_field_assertion = !matches!(assertion.assertion_type.as_str(), "error" | "not_error");
158 let effective_result_var = if has_field && is_error_context && returns_result && is_field_assertion {
159 format!("{result_var}_ok.as_ref().unwrap()")
161 } else {
162 result_var.to_string()
163 };
164
165 let field_access = match &assertion.field {
173 Some(f) if !f.is_empty() => {
174 if let Some((_, local_var)) = unwrapped_fields.iter().find(|(ff, _)| ff == f) {
175 local_var.clone()
176 } else if result_is_simple {
177 effective_result_var.clone()
180 } else if f == result_var {
181 effective_result_var.clone()
184 } else if result_is_tree {
185 tree_field_access_expr(f, &effective_result_var, module)
188 } else {
189 field_resolver.accessor(f, "rust", &effective_result_var)
190 }
191 }
192 _ => effective_result_var,
193 };
194
195 match assertion.assertion_type.as_str() {
196 "error" => {
197 let _ = writeln!(out, " assert!({result_var}.is_err(), \"expected call to fail\");");
198 if let Some(serde_json::Value::String(msg)) = &assertion.value {
199 let escaped = escape_rust(msg);
200 let _ = writeln!(
205 out,
206 " {{ let __e = {result_var}.as_ref().err().unwrap(); assert!(format!(\"{{:?}}\", __e).contains(\"{escaped}\") || __e.to_string().contains(\"{escaped}\"), \"error message mismatch\"); }}"
207 );
208 }
209 }
210 "not_error" => {
211 }
213 "equals" => {
214 render_equals_assertion(out, assertion, &field_access, is_unwrapped, field_resolver);
215 }
216 "contains" => {
217 if let Some(val) = &assertion.value {
218 let expected = value_to_rust_string(val);
219 let line = format!(
220 " assert!(format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected to contain: {{}}\", {expected});"
221 );
222 let _ = writeln!(out, "{line}");
223 }
224 }
225 "contains_all" => {
226 if let Some(values) = &assertion.values {
227 for val in values {
228 let expected = value_to_rust_string(val);
229 let line = format!(
230 " assert!(format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected to contain: {{}}\", {expected});"
231 );
232 let _ = writeln!(out, "{line}");
233 }
234 }
235 }
236 "not_contains" => {
237 if let Some(val) = &assertion.value {
238 let expected = value_to_rust_string(val);
239 let line = format!(
240 " assert!(!format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected NOT to contain: {{}}\", {expected});"
241 );
242 let _ = writeln!(out, "{line}");
243 }
244 }
245 "not_empty" => {
246 render_not_empty_assertion(
247 out,
248 assertion,
249 &field_access,
250 result_var,
251 result_is_option,
252 is_unwrapped,
253 field_resolver,
254 );
255 }
256 "is_empty" => {
257 render_is_empty_assertion(out, assertion, &field_access, is_unwrapped, field_resolver);
258 }
259 "contains_any" => {
260 if let Some(values) = &assertion.values {
261 let checks: Vec<String> = values
262 .iter()
263 .map(|v| {
264 let expected = value_to_rust_string(v);
265 format!("{field_access}.contains({expected})")
266 })
267 .collect();
268 let joined = checks.join(" || ");
269 let _ = writeln!(
270 out,
271 " assert!({joined}, \"expected to contain at least one of the specified values\");"
272 );
273 }
274 }
275 "greater_than" => {
276 if let Some(val) = &assertion.value {
277 if val.as_f64().is_some_and(|n| n < 0.0) {
279 let _ = writeln!(
280 out,
281 " // skipped: greater_than with negative value is always true for unsigned types"
282 );
283 } else if val.as_u64() == Some(0) {
284 if field_access.ends_with(".len()") {
285 let base = field_access.strip_suffix(".len()").unwrap();
287 let _ = writeln!(out, " assert!(!{base}.is_empty(), \"expected > 0\");");
288 } else {
289 let _ = writeln!(out, " assert!({field_access} > 0, \"expected > 0\");");
291 }
292 } else {
293 let lit = numeric_literal(val);
294 let _ = writeln!(out, " assert!({field_access} > {lit}, \"expected > {lit}\");");
295 }
296 }
297 }
298 "less_than" => {
299 if let Some(val) = &assertion.value {
300 let lit = numeric_literal(val);
301 let _ = writeln!(out, " assert!({field_access} < {lit}, \"expected < {lit}\");");
302 }
303 }
304 "greater_than_or_equal" => {
305 render_gte_assertion(out, assertion, &field_access, is_unwrapped, field_resolver);
306 }
307 "less_than_or_equal" => {
308 if let Some(val) = &assertion.value {
309 let lit = numeric_literal(val);
310 let _ = writeln!(out, " assert!({field_access} <= {lit}, \"expected <= {lit}\");");
311 }
312 }
313 "starts_with" => {
314 if let Some(val) = &assertion.value {
315 let expected = value_to_rust_string(val);
316 let _ = writeln!(
317 out,
318 " assert!({field_access}.starts_with({expected}), \"expected to start with: {{}}\", {expected});"
319 );
320 }
321 }
322 "ends_with" => {
323 if let Some(val) = &assertion.value {
324 let expected = value_to_rust_string(val);
325 let _ = writeln!(
326 out,
327 " assert!({field_access}.ends_with({expected}), \"expected to end with: {{}}\", {expected});"
328 );
329 }
330 }
331 "min_length" => {
332 if let Some(val) = &assertion.value {
333 if let Some(n) = val.as_u64() {
334 let _ = writeln!(
335 out,
336 " assert!({field_access}.len() >= {n}, \"expected length >= {n}, got {{}}\", {field_access}.len());"
337 );
338 }
339 }
340 }
341 "max_length" => {
342 if let Some(val) = &assertion.value {
343 if let Some(n) = val.as_u64() {
344 let _ = writeln!(
345 out,
346 " assert!({field_access}.len() <= {n}, \"expected length <= {n}, got {{}}\", {field_access}.len());"
347 );
348 }
349 }
350 }
351 "count_min" => {
352 render_count_min_assertion(out, assertion, &field_access, is_unwrapped, field_resolver);
353 }
354 "count_equals" => {
355 render_count_equals_assertion(out, assertion, &field_access, is_unwrapped, field_resolver);
356 }
357 "is_true" => {
358 let _ = writeln!(out, " assert!({field_access}, \"expected true\");");
359 }
360 "is_false" => {
361 let _ = writeln!(out, " assert!(!{field_access}, \"expected false\");");
362 }
363 "method_result" => {
364 render_method_result_assertion(out, assertion, &field_access, result_is_tree, module);
365 }
366 other => {
367 panic!("Rust e2e generator: unsupported assertion type: {other}");
368 }
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use std::collections::{HashMap, HashSet};
375
376 use super::*;
377 use crate::field_access::FieldResolver;
378 use crate::fixture::Assertion;
379
380 fn empty_resolver() -> FieldResolver {
381 FieldResolver::new(
382 &HashMap::new(),
383 &HashSet::new(),
384 &HashSet::new(),
385 &HashSet::new(),
386 &HashSet::new(),
387 )
388 }
389
390 fn make_assertion(assertion_type: &str, field: Option<&str>, value: Option<serde_json::Value>) -> Assertion {
391 Assertion {
392 assertion_type: assertion_type.to_string(),
393 field: field.map(|s| s.to_string()),
394 value,
395 ..Default::default()
396 }
397 }
398
399 #[test]
400 fn render_assertion_error_type_emits_is_err_check() {
401 let resolver = empty_resolver();
402 let assertion = make_assertion("error", None, None);
403 let mut out = String::new();
404 render_assertion(
405 &mut out,
406 &assertion,
407 "result",
408 "my_mod",
409 "dep",
410 true,
411 &[],
412 &resolver,
413 false,
414 false,
415 false,
416 false,
417 false,
418 );
419 assert!(out.contains("is_err()"), "got: {out}");
420 }
421
422 #[test]
423 fn render_assertion_vec_result_wraps_in_for_loop() {
424 let resolver = empty_resolver();
425 let assertion = make_assertion("not_empty", Some("content"), None);
426 let mut out = String::new();
427 render_assertion(
428 &mut out,
429 &assertion,
430 "result",
431 "my_mod",
432 "dep",
433 false,
434 &[],
435 &resolver,
436 false,
437 false,
438 true,
439 false,
440 false,
441 );
442 assert!(out.contains("for r in"), "got: {out}");
443 }
444
445 #[test]
446 fn render_assertion_not_empty_bare_result_uses_is_empty() {
447 let resolver = empty_resolver();
448 let assertion = make_assertion("not_empty", None, None);
449 let mut out = String::new();
450 render_assertion(
451 &mut out,
452 &assertion,
453 "result",
454 "my_mod",
455 "dep",
456 false,
457 &[],
458 &resolver,
459 false,
460 false,
461 false,
462 false,
463 false,
464 );
465 assert!(out.contains("is_empty()"), "got: {out}");
466 }
467}