1use crate::config::E2eConfig;
4use crate::escape::{go_string_literal, sanitize_filename};
5use crate::field_access::FieldResolver;
6use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup, HttpFixture};
7use alef_codegen::naming::{go_param_name, to_go_name};
8use alef_core::backend::GeneratedFile;
9use alef_core::config::AlefConfig;
10use alef_core::hash::{self, CommentStyle};
11use anyhow::Result;
12use heck::ToUpperCamelCase;
13use std::fmt::Write as FmtWrite;
14use std::path::PathBuf;
15
16use super::E2eCodegen;
17
18pub struct GoCodegen;
20
21impl E2eCodegen for GoCodegen {
22 fn generate(
23 &self,
24 groups: &[FixtureGroup],
25 e2e_config: &E2eConfig,
26 alef_config: &AlefConfig,
27 ) -> Result<Vec<GeneratedFile>> {
28 let lang = self.language_name();
29 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
30
31 let mut files = Vec::new();
32
33 let call = &e2e_config.call;
35 let overrides = call.overrides.get(lang);
36 let module_path = overrides
37 .and_then(|o| o.module.as_ref())
38 .cloned()
39 .unwrap_or_else(|| call.module.clone());
40 let import_alias = overrides
41 .and_then(|o| o.alias.as_ref())
42 .cloned()
43 .unwrap_or_else(|| "pkg".to_string());
44
45 let go_pkg = e2e_config.resolve_package("go");
47 let go_module_path = go_pkg
48 .as_ref()
49 .and_then(|p| p.module.as_ref())
50 .cloned()
51 .unwrap_or_else(|| module_path.clone());
52 let replace_path = go_pkg.as_ref().and_then(|p| p.path.as_ref()).cloned();
53 let go_version = go_pkg
54 .as_ref()
55 .and_then(|p| p.version.as_ref())
56 .cloned()
57 .unwrap_or_else(|| {
58 alef_config
59 .resolved_version()
60 .map(|v| format!("v{v}"))
61 .unwrap_or_else(|| "v0.0.0".to_string())
62 });
63 let field_resolver = FieldResolver::new(
64 &e2e_config.fields,
65 &e2e_config.fields_optional,
66 &e2e_config.result_fields,
67 &e2e_config.fields_array,
68 );
69
70 let effective_replace = match e2e_config.dep_mode {
73 crate::config::DependencyMode::Registry => None,
74 crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
75 };
76 files.push(GeneratedFile {
77 path: output_base.join("go.mod"),
78 content: render_go_mod(&go_module_path, effective_replace.as_deref(), &go_version),
79 generated_header: false,
80 });
81
82 for group in groups {
84 let active: Vec<&Fixture> = group
85 .fixtures
86 .iter()
87 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
88 .collect();
89
90 if active.is_empty() {
91 continue;
92 }
93
94 let filename = format!("{}_test.go", sanitize_filename(&group.category));
95 let content = render_test_file(
96 &group.category,
97 &active,
98 &module_path,
99 &import_alias,
100 &field_resolver,
101 e2e_config,
102 );
103 files.push(GeneratedFile {
104 path: output_base.join(filename),
105 content,
106 generated_header: true,
107 });
108 }
109
110 Ok(files)
111 }
112
113 fn language_name(&self) -> &'static str {
114 "go"
115 }
116}
117
118fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
119 let mut out = String::new();
120 let _ = writeln!(out, "module e2e_go");
121 let _ = writeln!(out);
122 let _ = writeln!(out, "go 1.26");
123 let _ = writeln!(out);
124 let _ = writeln!(out, "require (");
125 let _ = writeln!(out, "\t{go_module_path} {version}");
126 let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
127 let _ = writeln!(out, ")");
128
129 if let Some(path) = replace_path {
130 let _ = writeln!(out);
131 let _ = writeln!(out, "replace {go_module_path} => {path}");
132 }
133
134 out
135}
136
137fn render_test_file(
138 category: &str,
139 fixtures: &[&Fixture],
140 go_module_path: &str,
141 import_alias: &str,
142 field_resolver: &FieldResolver,
143 e2e_config: &crate::config::E2eConfig,
144) -> String {
145 let mut out = String::new();
146
147 out.push_str(&hash::header(CommentStyle::DoubleSlash));
149 let _ = writeln!(out);
150
151 let needs_pkg = fixtures.iter().any(|f| f.mock_response.is_some());
156
157 let needs_os = fixtures.iter().any(|f| {
160 if f.is_http_test() {
161 return true;
162 }
163 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
164 call_args.iter().any(|a| a.arg_type == "mock_url")
165 });
166
167 let needs_json = fixtures.iter().any(|f| {
170 let call = e2e_config.resolve_call(f.call.as_deref());
171 let call_args = &call.args;
172 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
174 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
175 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
176 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
177 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
178 })
179 };
180 let go_override = call.overrides.get("go");
182 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
183 e2e_config
184 .call
185 .overrides
186 .get("go")
187 .and_then(|o| o.options_type.as_deref())
188 });
189 let has_json_obj = call_args.iter().any(|a| {
190 if a.arg_type != "json_object" {
191 return false;
192 }
193 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
194 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
195 if v.is_array() {
196 return true;
197 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
199 });
200 has_handle || has_json_obj
201 });
202
203 let needs_base64 = fixtures.iter().any(|f| {
205 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
206 call_args.iter().any(|a| {
207 if a.arg_type != "bytes" {
208 return false;
209 }
210 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
211 matches!(f.input.get(field), Some(serde_json::Value::String(_)))
212 })
213 });
214
215 let needs_fmt = fixtures.iter().any(|f| {
217 f.visitor.as_ref().is_some_and(|v| {
218 v.callbacks.values().any(|action| {
219 if let CallbackAction::CustomTemplate { template } = action {
220 template.contains('{')
221 } else {
222 false
223 }
224 })
225 })
226 });
227
228 let needs_strings = fixtures.iter().any(|f| {
231 f.assertions.iter().any(|a| {
232 let type_needs_strings = if a.assertion_type == "equals" {
233 a.value.as_ref().is_some_and(|v| v.is_string())
235 } else {
236 matches!(
237 a.assertion_type.as_str(),
238 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
239 )
240 };
241 let field_valid = a
242 .field
243 .as_ref()
244 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
245 .unwrap_or(true);
246 type_needs_strings && field_valid
247 })
248 });
249
250 let needs_assert = fixtures.iter().any(|f| {
253 f.assertions.iter().any(|a| {
254 let field_valid = a
255 .field
256 .as_ref()
257 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
258 .unwrap_or(true);
259 let type_needs_assert = matches!(
260 a.assertion_type.as_str(),
261 "count_min"
262 | "count_max"
263 | "is_true"
264 | "is_false"
265 | "method_result"
266 | "min_length"
267 | "max_length"
268 | "matches_regex"
269 );
270 type_needs_assert && field_valid
271 })
272 });
273
274 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
276 let needs_http = has_http_fixtures;
277 let needs_io = fixtures
279 .iter()
280 .any(|f| f.http.as_ref().is_some_and(|h| h.expected_response.body.is_some()));
281
282 let needs_reflect = fixtures.iter().any(|f| {
284 if let Some(http) = &f.http {
285 if let Some(body) = &http.expected_response.body {
286 matches!(body, serde_json::Value::Object(_) | serde_json::Value::Array(_))
287 } else {
288 false
289 }
290 } else {
291 false
292 }
293 });
294
295 let _ = writeln!(out, "// E2e tests for category: {category}");
296 let _ = writeln!(out, "package e2e_test");
297 let _ = writeln!(out);
298 let _ = writeln!(out, "import (");
299 if needs_base64 {
300 let _ = writeln!(out, "\t\"encoding/base64\"");
301 }
302 if needs_json || needs_reflect {
303 let _ = writeln!(out, "\t\"encoding/json\"");
304 }
305 if needs_fmt {
306 let _ = writeln!(out, "\t\"fmt\"");
307 }
308 if needs_io {
309 let _ = writeln!(out, "\t\"io\"");
310 }
311 if needs_http {
312 let _ = writeln!(out, "\t\"net/http\"");
313 }
314 if needs_os {
315 let _ = writeln!(out, "\t\"os\"");
316 }
317 if needs_reflect {
318 let _ = writeln!(out, "\t\"reflect\"");
319 }
320 if needs_strings || needs_http {
321 let _ = writeln!(out, "\t\"strings\"");
322 }
323 let _ = writeln!(out, "\t\"testing\"");
324 if needs_assert {
325 let _ = writeln!(out);
326 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
327 }
328 if needs_pkg {
329 let _ = writeln!(out);
330 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
331 }
332 let _ = writeln!(out, ")");
333 let _ = writeln!(out);
334
335 for fixture in fixtures.iter() {
337 if let Some(visitor_spec) = &fixture.visitor {
338 let struct_name = visitor_struct_name(&fixture.id);
339 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
340 let _ = writeln!(out);
341 }
342 }
343
344 for (i, fixture) in fixtures.iter().enumerate() {
345 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
346 if i + 1 < fixtures.len() {
347 let _ = writeln!(out);
348 }
349 }
350
351 while out.ends_with("\n\n") {
353 out.pop();
354 }
355 if !out.ends_with('\n') {
356 out.push('\n');
357 }
358 out
359}
360
361fn render_test_function(
362 out: &mut String,
363 fixture: &Fixture,
364 import_alias: &str,
365 field_resolver: &FieldResolver,
366 e2e_config: &crate::config::E2eConfig,
367) {
368 let fn_name = fixture.id.to_upper_camel_case();
369 let description = &fixture.description;
370
371 if let Some(http) = &fixture.http {
373 render_http_test_function(out, fixture, http);
374 return;
375 }
376
377 if fixture.mock_response.is_none() {
382 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
383 let _ = writeln!(out, "\t// {description}");
384 let _ = writeln!(
385 out,
386 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
387 );
388 let _ = writeln!(out, "}}");
389 return;
390 }
391
392 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
394 let lang = "go";
395 let overrides = call_config.overrides.get(lang);
396 let function_name = to_go_name(
397 overrides
398 .and_then(|o| o.function.as_ref())
399 .map(String::as_str)
400 .unwrap_or(&call_config.function),
401 );
402 let result_var = &call_config.result_var;
403 let args = &call_config.args;
404
405 let returns_result = overrides
408 .and_then(|o| o.returns_result)
409 .unwrap_or(call_config.returns_result);
410
411 let returns_void = call_config.returns_void;
414
415 let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
418 call_config
419 .overrides
420 .get("rust")
421 .map(|o| o.result_is_simple)
422 .unwrap_or(false)
423 });
424
425 let result_is_array = overrides.map(|o| o.result_is_array).unwrap_or(false);
428
429 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
431 e2e_config
432 .call
433 .overrides
434 .get("go")
435 .and_then(|o| o.options_type.as_deref())
436 });
437
438 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
439
440 let (mut setup_lines, args_str) =
441 build_args_and_setup(&fixture.input, args, import_alias, call_options_type, &fixture.id);
442
443 let mut visitor_arg = String::new();
445 if fixture.visitor.is_some() {
446 let struct_name = visitor_struct_name(&fixture.id);
447 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
448 visitor_arg = "visitor".to_string();
449 }
450
451 let final_args = if visitor_arg.is_empty() {
452 args_str
453 } else {
454 format!("{args_str}, {visitor_arg}")
455 };
456
457 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
458 let _ = writeln!(out, "\t// {description}");
459
460 for line in &setup_lines {
461 let _ = writeln!(out, "\t{line}");
462 }
463
464 if expects_error {
465 if returns_result && !returns_void {
466 let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({final_args})");
467 } else {
468 let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
469 }
470 let _ = writeln!(out, "\tif err == nil {{");
471 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
472 let _ = writeln!(out, "\t}}");
473 let _ = writeln!(out, "}}");
474 return;
475 }
476
477 let has_usable_assertion = fixture.assertions.iter().any(|a| {
481 if a.assertion_type == "not_error" || a.assertion_type == "error" {
482 return false;
483 }
484 if a.assertion_type == "method_result" {
486 return true;
487 }
488 match &a.field {
489 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
490 _ => true,
491 }
492 });
493
494 if !returns_result && result_is_simple {
500 let result_binding = if has_usable_assertion {
502 result_var.to_string()
503 } else {
504 "_".to_string()
505 };
506 let assign_op = if result_binding == "_" { "=" } else { ":=" };
508 let _ = writeln!(
509 out,
510 "\t{result_binding} {assign_op} {import_alias}.{function_name}({final_args})"
511 );
512 if has_usable_assertion && result_binding != "_" {
513 let _ = writeln!(out, "\tif {result_var} == nil {{");
515 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
516 let _ = writeln!(out, "\t}}");
517 let _ = writeln!(out, "\tvalue := *{result_var}");
518 }
519 } else if !returns_result || returns_void {
520 let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
523 let _ = writeln!(out, "\tif err != nil {{");
524 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
525 let _ = writeln!(out, "\t}}");
526 let _ = writeln!(out, "}}");
528 return;
529 } else {
530 let result_binding = if has_usable_assertion {
532 result_var.to_string()
533 } else {
534 "_".to_string()
535 };
536 let _ = writeln!(
537 out,
538 "\t{result_binding}, err := {import_alias}.{function_name}({final_args})"
539 );
540 let _ = writeln!(out, "\tif err != nil {{");
541 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
542 let _ = writeln!(out, "\t}}");
543 if result_is_simple && has_usable_assertion && result_binding != "_" {
544 let _ = writeln!(out, "\tif {result_var} == nil {{");
546 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
547 let _ = writeln!(out, "\t}}");
548 let _ = writeln!(out, "\tvalue := *{result_var}");
549 }
550 }
551
552 let effective_result_var = if result_is_simple && has_usable_assertion {
554 "value".to_string()
555 } else {
556 result_var.to_string()
557 };
558
559 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
564 for assertion in &fixture.assertions {
565 if let Some(f) = &assertion.field {
566 if !f.is_empty() {
567 let resolved = field_resolver.resolve(f);
568 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
569 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
574 let is_array_field = field_resolver.is_array(resolved);
575 if !is_string_field || is_array_field {
576 continue;
579 }
580 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
581 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
582 if field_resolver.has_map_access(f) {
583 let _ = writeln!(out, "\t{local_var} := {field_expr}");
586 } else {
587 let _ = writeln!(out, "\tvar {local_var} string");
588 let _ = writeln!(out, "\tif {field_expr} != nil {{");
589 let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
590 let _ = writeln!(out, "\t}}");
591 }
592 optional_locals.insert(f.clone(), local_var);
593 }
594 }
595 }
596 }
597
598 for assertion in &fixture.assertions {
600 if let Some(f) = &assertion.field {
601 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
602 let parts: Vec<&str> = f.split('.').collect();
605 let mut guard_expr: Option<String> = None;
606 for i in 1..parts.len() {
607 let prefix = parts[..i].join(".");
608 let resolved_prefix = field_resolver.resolve(&prefix);
609 if field_resolver.is_optional(resolved_prefix) {
610 let accessor = field_resolver.accessor(&prefix, "go", &effective_result_var);
611 guard_expr = Some(accessor);
612 break;
613 }
614 }
615 if let Some(guard) = guard_expr {
616 if field_resolver.is_valid_for_result(f) {
619 let _ = writeln!(out, "\tif {guard} != nil {{");
620 let mut nil_buf = String::new();
623 render_assertion(
624 &mut nil_buf,
625 assertion,
626 &effective_result_var,
627 import_alias,
628 field_resolver,
629 &optional_locals,
630 result_is_simple,
631 result_is_array,
632 );
633 for line in nil_buf.lines() {
634 let _ = writeln!(out, "\t{line}");
635 }
636 let _ = writeln!(out, "\t}}");
637 } else {
638 render_assertion(
639 out,
640 assertion,
641 &effective_result_var,
642 import_alias,
643 field_resolver,
644 &optional_locals,
645 result_is_simple,
646 result_is_array,
647 );
648 }
649 continue;
650 }
651 }
652 }
653 render_assertion(
654 out,
655 assertion,
656 &effective_result_var,
657 import_alias,
658 field_resolver,
659 &optional_locals,
660 result_is_simple,
661 result_is_array,
662 );
663 }
664
665 let _ = writeln!(out, "}}");
666}
667
668fn render_http_test_function(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
673 let fn_name = fixture.id.to_upper_camel_case();
674 let description = &fixture.description;
675 let request = &http.request;
676 let expected = &http.expected_response;
677 let method = request.method.to_uppercase();
678 let fixture_id = &fixture.id;
679 let expected_status = expected.status_code;
680
681 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
682 let _ = writeln!(out, "\t// {description}");
683 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
684 let _ = writeln!(out, "\tif baseURL == \"\" {{");
685 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
686 let _ = writeln!(out, "\t}}");
687
688 let body_expr = if let Some(body) = &request.body {
690 let json = serde_json::to_string(body).unwrap_or_default();
691 let escaped = go_string_literal(&json);
692 format!("strings.NewReader({})", escaped)
693 } else {
694 "strings.NewReader(\"\")".to_string()
695 };
696
697 let _ = writeln!(out, "\tbody := {body_expr}");
698 let _ = writeln!(
699 out,
700 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"/fixtures/{fixture_id}\", body)"
701 );
702 let _ = writeln!(out, "\tif err != nil {{");
703 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
704 let _ = writeln!(out, "\t}}");
705
706 let content_type = request.content_type.as_deref().unwrap_or("application/json");
708 if request.body.is_some() {
709 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
710 }
711
712 for (name, value) in &request.headers {
713 let escaped_name = go_string_literal(name);
714 let escaped_value = go_string_literal(value);
715 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
716 }
717
718 if !request.cookies.is_empty() {
720 for (name, value) in &request.cookies {
721 let escaped_name = go_string_literal(name);
722 let escaped_value = go_string_literal(value);
723 let _ = writeln!(
724 out,
725 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
726 );
727 }
728 }
729
730 let _ = writeln!(out, "\tresp, err := http.DefaultClient.Do(req)");
732 let _ = writeln!(out, "\tif err != nil {{");
733 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
734 let _ = writeln!(out, "\t}}");
735 let _ = writeln!(out, "\tdefer resp.Body.Close()");
736
737 let body_used = expected.body.is_some();
739 if body_used {
740 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
741 let _ = writeln!(out, "\tif err != nil {{");
742 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
743 let _ = writeln!(out, "\t}}");
744 }
745
746 let _ = writeln!(out, "\tif resp.StatusCode != {expected_status} {{");
748 let _ = writeln!(
749 out,
750 "\t\tt.Fatalf(\"status: got %d want {expected_status}\", resp.StatusCode)"
751 );
752 let _ = writeln!(out, "\t}}");
753
754 if let Some(expected_body) = &expected.body {
756 match expected_body {
757 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
758 let json_str = serde_json::to_string(expected_body).unwrap_or_default();
759 let escaped = go_string_literal(&json_str);
760 let _ = writeln!(out, "\tvar got any");
762 let _ = writeln!(out, "\tvar want any");
763 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
764 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
765 let _ = writeln!(out, "\t}}");
766 let _ = writeln!(
767 out,
768 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
769 );
770 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
771 let _ = writeln!(out, "\t}}");
772 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
773 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
774 let _ = writeln!(out, "\t}}");
775 }
776 serde_json::Value::String(s) => {
777 let escaped = go_string_literal(s);
778 let _ = writeln!(out, "\twant := {escaped}");
779 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
780 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
781 let _ = writeln!(out, "\t}}");
782 }
783 other => {
784 let escaped = go_string_literal(&other.to_string());
785 let _ = writeln!(out, "\twant := {escaped}");
786 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
787 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
788 let _ = writeln!(out, "\t}}");
789 }
790 }
791 }
792
793 for (name, value) in &expected.headers {
795 if value == "<<absent>>" || value == "<<present>>" || value == "<<uuid>>" {
796 continue;
798 }
799 let escaped_name = go_string_literal(name);
800 let escaped_value = go_string_literal(value);
801 let _ = writeln!(
802 out,
803 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
804 );
805 let _ = writeln!(
806 out,
807 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
808 );
809 let _ = writeln!(out, "\t}}");
810 }
811
812 let _ = writeln!(out, "}}");
813}
814
815fn build_args_and_setup(
819 input: &serde_json::Value,
820 args: &[crate::config::ArgMapping],
821 import_alias: &str,
822 options_type: Option<&str>,
823 fixture_id: &str,
824) -> (Vec<String>, String) {
825 use heck::ToUpperCamelCase;
826
827 if args.is_empty() {
828 return (Vec::new(), String::new());
829 }
830
831 let mut setup_lines: Vec<String> = Vec::new();
832 let mut parts: Vec<String> = Vec::new();
833
834 for arg in args {
835 if arg.arg_type == "mock_url" {
836 setup_lines.push(format!(
837 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
838 arg.name,
839 ));
840 parts.push(arg.name.clone());
841 continue;
842 }
843
844 if arg.arg_type == "handle" {
845 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
847 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
848 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
849 if config_value.is_null()
850 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
851 {
852 setup_lines.push(format!(
853 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
854 name = arg.name,
855 ));
856 } else {
857 let json_str = serde_json::to_string(config_value).unwrap_or_default();
858 let go_literal = go_string_literal(&json_str);
859 let name = &arg.name;
860 setup_lines.push(format!(
861 "var {name}Config {import_alias}.CrawlConfig\n\tif err := json.Unmarshal([]byte({go_literal}), &{name}Config); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
862 ));
863 setup_lines.push(format!(
864 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
865 ));
866 }
867 parts.push(arg.name.clone());
868 continue;
869 }
870
871 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
872 let val = input.get(field);
873
874 if arg.arg_type == "bytes" {
877 let var_name = format!("{}Bytes", arg.name);
878 match val {
879 None | Some(serde_json::Value::Null) => {
880 if arg.optional {
881 parts.push("nil".to_string());
882 } else {
883 parts.push("[]byte{}".to_string());
884 }
885 }
886 Some(serde_json::Value::String(s)) => {
887 let go_b64 = go_string_literal(s);
888 setup_lines.push(format!("{var_name}, _ := base64.StdEncoding.DecodeString({go_b64})"));
889 parts.push(var_name);
890 }
891 Some(other) => {
892 parts.push(format!("[]byte({})", json_to_go(other)));
893 }
894 }
895 continue;
896 }
897
898 match val {
899 None | Some(serde_json::Value::Null) if arg.optional => {
900 match arg.arg_type.as_str() {
902 "string" => {
903 parts.push("nil".to_string());
905 }
906 "json_object" => {
907 if let Some(opts_type) = options_type {
909 parts.push(format!("{import_alias}.{opts_type}{{}}"));
910 } else {
911 parts.push("nil".to_string());
912 }
913 }
914 _ => {
915 parts.push("nil".to_string());
916 }
917 }
918 }
919 None | Some(serde_json::Value::Null) => {
920 let default_val = match arg.arg_type.as_str() {
922 "string" => "\"\"".to_string(),
923 "int" | "integer" | "i64" => "0".to_string(),
924 "float" | "number" => "0.0".to_string(),
925 "bool" | "boolean" => "false".to_string(),
926 "json_object" => {
927 if let Some(opts_type) = options_type {
928 format!("{import_alias}.{opts_type}{{}}")
929 } else {
930 "nil".to_string()
931 }
932 }
933 _ => "nil".to_string(),
934 };
935 parts.push(default_val);
936 }
937 Some(v) => {
938 match arg.arg_type.as_str() {
939 "json_object" => {
940 let is_array = v.is_array();
943 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
944 if is_empty_obj {
945 if let Some(opts_type) = options_type {
946 parts.push(format!("{import_alias}.{opts_type}{{}}"));
947 } else {
948 parts.push("nil".to_string());
949 }
950 } else if is_array {
951 let json_str = serde_json::to_string(v).unwrap_or_default();
953 let go_literal = go_string_literal(&json_str);
954 let var_name = &arg.name;
955 setup_lines.push(format!(
956 "var {var_name} []string\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
957 ));
958 parts.push(var_name.to_string());
959 } else if let Some(opts_type) = options_type {
960 let json_str = serde_json::to_string(v).unwrap_or_default();
962 let go_literal = go_string_literal(&json_str);
963 let var_name = &arg.name;
964 setup_lines.push(format!(
965 "var {var_name} {import_alias}.{opts_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
966 ));
967 parts.push(var_name.to_string());
968 } else {
969 parts.push(json_to_go(v));
970 }
971 }
972 "string" if arg.optional => {
973 let var_name = format!("{}Val", arg.name);
975 let go_val = json_to_go(v);
976 setup_lines.push(format!("{var_name} := {go_val}"));
977 parts.push(format!("&{var_name}"));
978 }
979 _ => {
980 parts.push(json_to_go(v));
981 }
982 }
983 }
984 }
985 }
986
987 (setup_lines, parts.join(", "))
988}
989
990#[allow(clippy::too_many_arguments)]
991fn render_assertion(
992 out: &mut String,
993 assertion: &Assertion,
994 result_var: &str,
995 import_alias: &str,
996 field_resolver: &FieldResolver,
997 optional_locals: &std::collections::HashMap<String, String>,
998 result_is_simple: bool,
999 result_is_array: bool,
1000) {
1001 if !result_is_simple {
1004 if let Some(f) = &assertion.field {
1005 let embed_deref = format!("(*{result_var})");
1008 match f.as_str() {
1009 "chunks_have_content" => {
1010 let pred = format!(
1011 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
1012 );
1013 match assertion.assertion_type.as_str() {
1014 "is_true" => {
1015 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1016 }
1017 "is_false" => {
1018 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1019 }
1020 _ => {
1021 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1022 }
1023 }
1024 return;
1025 }
1026 "chunks_have_embeddings" => {
1027 let pred = format!(
1028 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Embedding == nil || len(*c.Embedding) == 0 {{ return false }} }}; return true }}()"
1029 );
1030 match assertion.assertion_type.as_str() {
1031 "is_true" => {
1032 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1033 }
1034 "is_false" => {
1035 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1036 }
1037 _ => {
1038 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1039 }
1040 }
1041 return;
1042 }
1043 "embeddings" => {
1044 match assertion.assertion_type.as_str() {
1045 "count_equals" => {
1046 if let Some(val) = &assertion.value {
1047 if let Some(n) = val.as_u64() {
1048 let _ = writeln!(
1049 out,
1050 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1051 );
1052 }
1053 }
1054 }
1055 "count_min" => {
1056 if let Some(val) = &assertion.value {
1057 if let Some(n) = val.as_u64() {
1058 let _ = writeln!(
1059 out,
1060 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1061 );
1062 }
1063 }
1064 }
1065 "not_empty" => {
1066 let _ = writeln!(
1067 out,
1068 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1069 );
1070 }
1071 "is_empty" => {
1072 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1073 }
1074 _ => {
1075 let _ = writeln!(
1076 out,
1077 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1078 );
1079 }
1080 }
1081 return;
1082 }
1083 "embedding_dimensions" => {
1084 let expr = format!(
1085 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1086 );
1087 match assertion.assertion_type.as_str() {
1088 "equals" => {
1089 if let Some(val) = &assertion.value {
1090 if let Some(n) = val.as_u64() {
1091 let _ = writeln!(
1092 out,
1093 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1094 );
1095 }
1096 }
1097 }
1098 "greater_than" => {
1099 if let Some(val) = &assertion.value {
1100 if let Some(n) = val.as_u64() {
1101 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
1102 }
1103 }
1104 }
1105 _ => {
1106 let _ = writeln!(
1107 out,
1108 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
1109 );
1110 }
1111 }
1112 return;
1113 }
1114 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1115 let pred = match f.as_str() {
1116 "embeddings_valid" => {
1117 format!(
1118 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
1119 )
1120 }
1121 "embeddings_finite" => {
1122 format!(
1123 "func() bool {{ for _, e := range {embed_deref} {{ for _, v := range e {{ if v != v || v == float32(1.0/0.0) || v == float32(-1.0/0.0) {{ return false }} }} }}; return true }}()"
1124 )
1125 }
1126 "embeddings_non_zero" => {
1127 format!(
1128 "func() bool {{ for _, e := range {embed_deref} {{ hasNonZero := false; for _, v := range e {{ if v != 0 {{ hasNonZero = true; break }} }}; if !hasNonZero {{ return false }} }}; return true }}()"
1129 )
1130 }
1131 "embeddings_normalized" => {
1132 format!(
1133 "func() bool {{ for _, e := range {embed_deref} {{ var n float64; for _, v := range e {{ n += float64(v) * float64(v) }}; if n < 0.999 || n > 1.001 {{ return false }} }}; return true }}()"
1134 )
1135 }
1136 _ => unreachable!(),
1137 };
1138 match assertion.assertion_type.as_str() {
1139 "is_true" => {
1140 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1141 }
1142 "is_false" => {
1143 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1144 }
1145 _ => {
1146 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1147 }
1148 }
1149 return;
1150 }
1151 "keywords" | "keywords_count" => {
1154 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
1155 return;
1156 }
1157 _ => {}
1158 }
1159 }
1160 }
1161
1162 if !result_is_simple {
1165 if let Some(f) = &assertion.field {
1166 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1167 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
1168 return;
1169 }
1170 }
1171 }
1172
1173 let field_expr = if result_is_simple {
1174 result_var.to_string()
1176 } else {
1177 match &assertion.field {
1178 Some(f) if !f.is_empty() => {
1179 if let Some(local_var) = optional_locals.get(f.as_str()) {
1181 local_var.clone()
1182 } else {
1183 field_resolver.accessor(f, "go", result_var)
1184 }
1185 }
1186 _ => result_var.to_string(),
1187 }
1188 };
1189
1190 let is_optional = assertion
1194 .field
1195 .as_ref()
1196 .map(|f| {
1197 let resolved = field_resolver.resolve(f);
1198 let check_path = resolved
1199 .strip_suffix(".length")
1200 .or_else(|| resolved.strip_suffix(".count"))
1201 .or_else(|| resolved.strip_suffix(".size"))
1202 .unwrap_or(resolved);
1203 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
1204 })
1205 .unwrap_or(false);
1206
1207 let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
1210 let inner = &field_expr[4..field_expr.len() - 1];
1211 format!("len(*{inner})")
1212 } else {
1213 field_expr
1214 };
1215 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
1217 Some(field_expr[5..field_expr.len() - 1].to_string())
1218 } else {
1219 None
1220 };
1221
1222 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
1225 format!("*{field_expr}")
1226 } else {
1227 field_expr.clone()
1228 };
1229
1230 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
1235 let array_expr = &field_expr[..idx];
1236 Some(array_expr.to_string())
1237 } else {
1238 None
1239 };
1240
1241 let mut assertion_buf = String::new();
1244 let out_ref = &mut assertion_buf;
1245
1246 match assertion.assertion_type.as_str() {
1247 "equals" => {
1248 if let Some(expected) = &assertion.value {
1249 let go_val = json_to_go(expected);
1250 if expected.is_string() {
1252 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
1254 format!("strings.TrimSpace(*{field_expr})")
1255 } else {
1256 format!("strings.TrimSpace({field_expr})")
1257 };
1258 if is_optional && !field_expr.starts_with("len(") {
1259 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
1260 } else {
1261 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
1262 }
1263 } else if is_optional && !field_expr.starts_with("len(") {
1264 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
1265 } else {
1266 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
1267 }
1268 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
1269 let _ = writeln!(out_ref, "\t}}");
1270 }
1271 }
1272 "contains" => {
1273 if let Some(expected) = &assertion.value {
1274 let go_val = json_to_go(expected);
1275 let resolved_field = assertion.field.as_deref().unwrap_or("");
1281 let resolved_name = field_resolver.resolve(resolved_field);
1282 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1283 let is_opt =
1284 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1285 let field_for_contains = if is_opt && field_is_array {
1286 format!("strings.Join(*{field_expr}, \" \")")
1287 } else if is_opt {
1288 format!("string(*{field_expr})")
1289 } else if field_is_array {
1290 format!("strings.Join({field_expr}, \" \")")
1291 } else {
1292 format!("string({field_expr})")
1293 };
1294 if is_opt {
1295 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1296 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1297 let _ = writeln!(
1298 out_ref,
1299 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1300 );
1301 let _ = writeln!(out_ref, "\t}}");
1302 let _ = writeln!(out_ref, "\t}}");
1303 } else {
1304 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1305 let _ = writeln!(
1306 out_ref,
1307 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1308 );
1309 let _ = writeln!(out_ref, "\t}}");
1310 }
1311 }
1312 }
1313 "contains_all" => {
1314 if let Some(values) = &assertion.values {
1315 let resolved_field = assertion.field.as_deref().unwrap_or("");
1316 let resolved_name = field_resolver.resolve(resolved_field);
1317 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1318 let is_opt =
1319 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1320 for val in values {
1321 let go_val = json_to_go(val);
1322 let field_for_contains = if is_opt && field_is_array {
1323 format!("strings.Join(*{field_expr}, \" \")")
1324 } else if is_opt {
1325 format!("string(*{field_expr})")
1326 } else if field_is_array {
1327 format!("strings.Join({field_expr}, \" \")")
1328 } else {
1329 format!("string({field_expr})")
1330 };
1331 if is_opt {
1332 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1333 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1334 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1335 let _ = writeln!(out_ref, "\t}}");
1336 let _ = writeln!(out_ref, "\t}}");
1337 } else {
1338 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1339 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1340 let _ = writeln!(out_ref, "\t}}");
1341 }
1342 }
1343 }
1344 }
1345 "not_contains" => {
1346 if let Some(expected) = &assertion.value {
1347 let go_val = json_to_go(expected);
1348 let resolved_field = assertion.field.as_deref().unwrap_or("");
1349 let resolved_name = field_resolver.resolve(resolved_field);
1350 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1351 let is_opt =
1352 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1353 let field_for_contains = if is_opt && field_is_array {
1354 format!("strings.Join(*{field_expr}, \" \")")
1355 } else if is_opt {
1356 format!("string(*{field_expr})")
1357 } else if field_is_array {
1358 format!("strings.Join({field_expr}, \" \")")
1359 } else {
1360 format!("string({field_expr})")
1361 };
1362 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
1363 let _ = writeln!(
1364 out_ref,
1365 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
1366 );
1367 let _ = writeln!(out_ref, "\t}}");
1368 }
1369 }
1370 "not_empty" => {
1371 let field_is_array = {
1374 let rf = assertion.field.as_deref().unwrap_or("");
1375 let rn = field_resolver.resolve(rf);
1376 field_resolver.is_array(rn)
1377 };
1378 if is_optional && !field_is_array {
1379 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
1381 } else if is_optional {
1382 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
1383 } else {
1384 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
1385 }
1386 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
1387 let _ = writeln!(out_ref, "\t}}");
1388 }
1389 "is_empty" => {
1390 let field_is_array = {
1391 let rf = assertion.field.as_deref().unwrap_or("");
1392 let rn = field_resolver.resolve(rf);
1393 field_resolver.is_array(rn)
1394 };
1395 if is_optional && !field_is_array {
1396 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1398 } else if is_optional {
1399 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
1400 } else {
1401 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
1402 }
1403 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
1404 let _ = writeln!(out_ref, "\t}}");
1405 }
1406 "contains_any" => {
1407 if let Some(values) = &assertion.values {
1408 let resolved_field = assertion.field.as_deref().unwrap_or("");
1409 let resolved_name = field_resolver.resolve(resolved_field);
1410 let field_is_array = field_resolver.is_array(resolved_name);
1411 let is_opt =
1412 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1413 let field_for_contains = if is_opt && field_is_array {
1414 format!("strings.Join(*{field_expr}, \" \")")
1415 } else if is_opt {
1416 format!("*{field_expr}")
1417 } else if field_is_array {
1418 format!("strings.Join({field_expr}, \" \")")
1419 } else {
1420 field_expr.clone()
1421 };
1422 let _ = writeln!(out_ref, "\t{{");
1423 let _ = writeln!(out_ref, "\t\tfound := false");
1424 for val in values {
1425 let go_val = json_to_go(val);
1426 let _ = writeln!(
1427 out_ref,
1428 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
1429 );
1430 }
1431 let _ = writeln!(out_ref, "\t\tif !found {{");
1432 let _ = writeln!(
1433 out_ref,
1434 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
1435 );
1436 let _ = writeln!(out_ref, "\t\t}}");
1437 let _ = writeln!(out_ref, "\t}}");
1438 }
1439 }
1440 "greater_than" => {
1441 if let Some(val) = &assertion.value {
1442 let go_val = json_to_go(val);
1443 if is_optional {
1447 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1448 if let Some(n) = val.as_u64() {
1449 let next = n + 1;
1450 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
1451 } else {
1452 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
1453 }
1454 let _ = writeln!(
1455 out_ref,
1456 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
1457 );
1458 let _ = writeln!(out_ref, "\t\t}}");
1459 let _ = writeln!(out_ref, "\t}}");
1460 } else if let Some(n) = val.as_u64() {
1461 let next = n + 1;
1462 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
1463 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1464 let _ = writeln!(out_ref, "\t}}");
1465 } else {
1466 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
1467 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1468 let _ = writeln!(out_ref, "\t}}");
1469 }
1470 }
1471 }
1472 "less_than" => {
1473 if let Some(val) = &assertion.value {
1474 let go_val = json_to_go(val);
1475 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
1476 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
1477 let _ = writeln!(out_ref, "\t}}");
1478 }
1479 }
1480 "greater_than_or_equal" => {
1481 if let Some(val) = &assertion.value {
1482 let go_val = json_to_go(val);
1483 if let Some(ref guard) = nil_guard_expr {
1484 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
1485 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
1486 let _ = writeln!(
1487 out_ref,
1488 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
1489 );
1490 let _ = writeln!(out_ref, "\t\t}}");
1491 let _ = writeln!(out_ref, "\t}}");
1492 } else if is_optional && !field_expr.starts_with("len(") {
1493 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1495 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
1496 let _ = writeln!(
1497 out_ref,
1498 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
1499 );
1500 let _ = writeln!(out_ref, "\t\t}}");
1501 let _ = writeln!(out_ref, "\t}}");
1502 } else {
1503 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
1504 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
1505 let _ = writeln!(out_ref, "\t}}");
1506 }
1507 }
1508 }
1509 "less_than_or_equal" => {
1510 if let Some(val) = &assertion.value {
1511 let go_val = json_to_go(val);
1512 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
1513 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
1514 let _ = writeln!(out_ref, "\t}}");
1515 }
1516 }
1517 "starts_with" => {
1518 if let Some(expected) = &assertion.value {
1519 let go_val = json_to_go(expected);
1520 let field_for_prefix = if is_optional
1521 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1522 {
1523 format!("string(*{field_expr})")
1524 } else {
1525 format!("string({field_expr})")
1526 };
1527 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
1528 let _ = writeln!(
1529 out_ref,
1530 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
1531 );
1532 let _ = writeln!(out_ref, "\t}}");
1533 }
1534 }
1535 "count_min" => {
1536 if let Some(val) = &assertion.value {
1537 if let Some(n) = val.as_u64() {
1538 if is_optional {
1539 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1540 let _ = writeln!(
1541 out_ref,
1542 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
1543 );
1544 let _ = writeln!(out_ref, "\t}}");
1545 } else {
1546 let _ = writeln!(
1547 out_ref,
1548 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
1549 );
1550 }
1551 }
1552 }
1553 }
1554 "count_equals" => {
1555 if let Some(val) = &assertion.value {
1556 if let Some(n) = val.as_u64() {
1557 if is_optional {
1558 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1559 let _ = writeln!(
1560 out_ref,
1561 "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
1562 );
1563 let _ = writeln!(out_ref, "\t}}");
1564 } else {
1565 let _ = writeln!(
1566 out_ref,
1567 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
1568 );
1569 }
1570 }
1571 }
1572 }
1573 "is_true" => {
1574 if is_optional {
1575 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1576 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
1577 let _ = writeln!(out_ref, "\t}}");
1578 } else {
1579 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
1580 }
1581 }
1582 "is_false" => {
1583 if is_optional {
1584 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1585 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
1586 let _ = writeln!(out_ref, "\t}}");
1587 } else {
1588 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
1589 }
1590 }
1591 "method_result" => {
1592 if let Some(method_name) = &assertion.method {
1593 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
1594 let check = assertion.check.as_deref().unwrap_or("is_true");
1595 let deref_expr = if info.is_pointer {
1598 format!("*{}", info.call_expr)
1599 } else {
1600 info.call_expr.clone()
1601 };
1602 match check {
1603 "equals" => {
1604 if let Some(val) = &assertion.value {
1605 if val.is_boolean() {
1606 if val.as_bool() == Some(true) {
1607 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1608 } else {
1609 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1610 }
1611 } else {
1612 let go_val = if let Some(cast) = info.value_cast {
1616 if val.is_number() {
1617 format!("{cast}({})", json_to_go(val))
1618 } else {
1619 json_to_go(val)
1620 }
1621 } else {
1622 json_to_go(val)
1623 };
1624 let _ = writeln!(
1625 out_ref,
1626 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
1627 );
1628 }
1629 }
1630 }
1631 "is_true" => {
1632 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1633 }
1634 "is_false" => {
1635 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1636 }
1637 "greater_than_or_equal" => {
1638 if let Some(val) = &assertion.value {
1639 let n = val.as_u64().unwrap_or(0);
1640 let cast = info.value_cast.unwrap_or("uint");
1642 let _ = writeln!(
1643 out_ref,
1644 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
1645 );
1646 }
1647 }
1648 "count_min" => {
1649 if let Some(val) = &assertion.value {
1650 let n = val.as_u64().unwrap_or(0);
1651 let _ = writeln!(
1652 out_ref,
1653 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
1654 );
1655 }
1656 }
1657 "contains" => {
1658 if let Some(val) = &assertion.value {
1659 let go_val = json_to_go(val);
1660 let _ = writeln!(
1661 out_ref,
1662 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
1663 );
1664 }
1665 }
1666 "is_error" => {
1667 let _ = writeln!(out_ref, "\t{{");
1668 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
1669 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
1670 let _ = writeln!(out_ref, "\t}}");
1671 }
1672 other_check => {
1673 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
1674 }
1675 }
1676 } else {
1677 panic!("Go e2e generator: method_result assertion missing 'method' field");
1678 }
1679 }
1680 "min_length" => {
1681 if let Some(val) = &assertion.value {
1682 if let Some(n) = val.as_u64() {
1683 if is_optional {
1684 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1685 let _ = writeln!(
1686 out_ref,
1687 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
1688 );
1689 let _ = writeln!(out_ref, "\t}}");
1690 } else {
1691 let _ = writeln!(
1692 out_ref,
1693 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
1694 );
1695 }
1696 }
1697 }
1698 }
1699 "max_length" => {
1700 if let Some(val) = &assertion.value {
1701 if let Some(n) = val.as_u64() {
1702 if is_optional {
1703 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1704 let _ = writeln!(
1705 out_ref,
1706 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
1707 );
1708 let _ = writeln!(out_ref, "\t}}");
1709 } else {
1710 let _ = writeln!(
1711 out_ref,
1712 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
1713 );
1714 }
1715 }
1716 }
1717 }
1718 "ends_with" => {
1719 if let Some(expected) = &assertion.value {
1720 let go_val = json_to_go(expected);
1721 let field_for_suffix = if is_optional
1722 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1723 {
1724 format!("string(*{field_expr})")
1725 } else {
1726 format!("string({field_expr})")
1727 };
1728 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
1729 let _ = writeln!(
1730 out_ref,
1731 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
1732 );
1733 let _ = writeln!(out_ref, "\t}}");
1734 }
1735 }
1736 "matches_regex" => {
1737 if let Some(expected) = &assertion.value {
1738 let go_val = json_to_go(expected);
1739 let field_for_regex = if is_optional
1740 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1741 {
1742 format!("*{field_expr}")
1743 } else {
1744 field_expr.clone()
1745 };
1746 let _ = writeln!(
1747 out_ref,
1748 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
1749 );
1750 }
1751 }
1752 "not_error" => {
1753 }
1755 "error" => {
1756 }
1758 other => {
1759 panic!("Go e2e generator: unsupported assertion type: {other}");
1760 }
1761 }
1762
1763 if let Some(ref arr) = array_guard {
1766 if !assertion_buf.is_empty() {
1767 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
1768 for line in assertion_buf.lines() {
1770 let _ = writeln!(out, "\t{line}");
1771 }
1772 let _ = writeln!(out, "\t}}");
1773 }
1774 } else {
1775 out.push_str(&assertion_buf);
1776 }
1777}
1778
1779struct GoMethodCallInfo {
1781 call_expr: String,
1783 is_pointer: bool,
1785 value_cast: Option<&'static str>,
1788}
1789
1790fn build_go_method_call(
1805 result_var: &str,
1806 method_name: &str,
1807 args: Option<&serde_json::Value>,
1808 import_alias: &str,
1809) -> GoMethodCallInfo {
1810 match method_name {
1811 "root_node_type" => GoMethodCallInfo {
1812 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
1813 is_pointer: false,
1814 value_cast: None,
1815 },
1816 "named_children_count" => GoMethodCallInfo {
1817 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
1818 is_pointer: false,
1819 value_cast: Some("uint"),
1820 },
1821 "has_error_nodes" => GoMethodCallInfo {
1822 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
1823 is_pointer: true,
1824 value_cast: None,
1825 },
1826 "error_count" | "tree_error_count" => GoMethodCallInfo {
1827 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
1828 is_pointer: true,
1829 value_cast: Some("uint"),
1830 },
1831 "tree_to_sexp" => GoMethodCallInfo {
1832 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
1833 is_pointer: true,
1834 value_cast: None,
1835 },
1836 "contains_node_type" => {
1837 let node_type = args
1838 .and_then(|a| a.get("node_type"))
1839 .and_then(|v| v.as_str())
1840 .unwrap_or("");
1841 GoMethodCallInfo {
1842 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
1843 is_pointer: true,
1844 value_cast: None,
1845 }
1846 }
1847 "find_nodes_by_type" => {
1848 let node_type = args
1849 .and_then(|a| a.get("node_type"))
1850 .and_then(|v| v.as_str())
1851 .unwrap_or("");
1852 GoMethodCallInfo {
1853 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
1854 is_pointer: true,
1855 value_cast: None,
1856 }
1857 }
1858 "run_query" => {
1859 let query_source = args
1860 .and_then(|a| a.get("query_source"))
1861 .and_then(|v| v.as_str())
1862 .unwrap_or("");
1863 let language = args
1864 .and_then(|a| a.get("language"))
1865 .and_then(|v| v.as_str())
1866 .unwrap_or("");
1867 let query_lit = go_string_literal(query_source);
1868 let lang_lit = go_string_literal(language);
1869 GoMethodCallInfo {
1871 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
1872 is_pointer: false,
1873 value_cast: None,
1874 }
1875 }
1876 other => {
1877 let method_pascal = other.to_upper_camel_case();
1878 GoMethodCallInfo {
1879 call_expr: format!("{result_var}.{method_pascal}()"),
1880 is_pointer: false,
1881 value_cast: None,
1882 }
1883 }
1884 }
1885}
1886
1887fn json_to_go(value: &serde_json::Value) -> String {
1889 match value {
1890 serde_json::Value::String(s) => go_string_literal(s),
1891 serde_json::Value::Bool(b) => b.to_string(),
1892 serde_json::Value::Number(n) => n.to_string(),
1893 serde_json::Value::Null => "nil".to_string(),
1894 other => go_string_literal(&other.to_string()),
1896 }
1897}
1898
1899fn visitor_struct_name(fixture_id: &str) -> String {
1908 use heck::ToUpperCamelCase;
1909 format!("testVisitor{}", fixture_id.to_upper_camel_case())
1911}
1912
1913fn emit_go_visitor_struct(
1915 out: &mut String,
1916 struct_name: &str,
1917 visitor_spec: &crate::fixture::VisitorSpec,
1918 import_alias: &str,
1919) {
1920 let _ = writeln!(out, "type {struct_name} struct{{}}");
1921 for (method_name, action) in &visitor_spec.callbacks {
1922 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
1923 }
1924}
1925
1926fn emit_go_visitor_method(
1928 out: &mut String,
1929 struct_name: &str,
1930 method_name: &str,
1931 action: &CallbackAction,
1932 import_alias: &str,
1933) {
1934 let camel_method = method_to_camel(method_name);
1935 let params = match method_name {
1936 "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
1937 "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
1938 "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
1939 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
1940 "visit_code_inline"
1941 | "visit_strong"
1942 | "visit_emphasis"
1943 | "visit_strikethrough"
1944 | "visit_underline"
1945 | "visit_subscript"
1946 | "visit_superscript"
1947 | "visit_mark"
1948 | "visit_button"
1949 | "visit_summary"
1950 | "visit_figcaption"
1951 | "visit_definition_term"
1952 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
1953 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
1954 "visit_list_item" => {
1955 format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
1956 }
1957 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
1958 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
1959 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
1960 "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
1961 "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
1962 "visit_audio" | "visit_video" | "visit_iframe" => {
1963 format!("_ {import_alias}.NodeContext, src string")
1964 }
1965 "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
1966 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1967 format!("_ {import_alias}.NodeContext, output string")
1968 }
1969 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
1970 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
1971 _ => format!("_ {import_alias}.NodeContext"),
1972 };
1973
1974 let _ = writeln!(
1975 out,
1976 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1977 );
1978 match action {
1979 CallbackAction::Skip => {
1980 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1981 }
1982 CallbackAction::Continue => {
1983 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
1984 }
1985 CallbackAction::PreserveHtml => {
1986 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
1987 }
1988 CallbackAction::Custom { output } => {
1989 let escaped = go_string_literal(output);
1990 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
1991 }
1992 CallbackAction::CustomTemplate { template } => {
1993 let (fmt_str, fmt_args) = template_to_sprintf(template);
1996 let escaped_fmt = go_string_literal(&fmt_str);
1997 if fmt_args.is_empty() {
1998 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
1999 } else {
2000 let args_str = fmt_args.join(", ");
2001 let _ = writeln!(
2002 out,
2003 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
2004 );
2005 }
2006 }
2007 }
2008 let _ = writeln!(out, "}}");
2009}
2010
2011fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
2015 let mut fmt_str = String::new();
2016 let mut args: Vec<String> = Vec::new();
2017 let mut chars = template.chars().peekable();
2018 while let Some(c) = chars.next() {
2019 if c == '{' {
2020 let mut name = String::new();
2022 for inner in chars.by_ref() {
2023 if inner == '}' {
2024 break;
2025 }
2026 name.push(inner);
2027 }
2028 fmt_str.push_str("%s");
2029 args.push(name);
2030 } else {
2031 fmt_str.push(c);
2032 }
2033 }
2034 (fmt_str, args)
2035}
2036
2037fn method_to_camel(snake: &str) -> String {
2039 use heck::ToUpperCamelCase;
2040 snake.to_upper_camel_case()
2041}
2042
2043#[cfg(test)]
2044mod tests {
2045 use super::*;
2046 use crate::config::{CallConfig, E2eConfig};
2047 use crate::field_access::FieldResolver;
2048 use crate::fixture::{Assertion, Fixture};
2049
2050 fn make_fixture(id: &str) -> Fixture {
2051 Fixture {
2052 id: id.to_string(),
2053 category: None,
2054 description: "test fixture".to_string(),
2055 tags: vec![],
2056 skip: None,
2057 call: None,
2058 input: serde_json::Value::Null,
2059 mock_response: None,
2060 source: String::new(),
2061 http: None,
2062 assertions: vec![Assertion {
2063 assertion_type: "not_error".to_string(),
2064 field: None,
2065 value: None,
2066 values: None,
2067 method: None,
2068 args: None,
2069 check: None,
2070 }],
2071 visitor: None,
2072 }
2073 }
2074
2075 #[test]
2079 fn test_go_method_name_uses_go_casing() {
2080 let e2e_config = E2eConfig {
2081 call: CallConfig {
2082 function: "clean_extracted_text".to_string(),
2083 module: "github.com/example/mylib".to_string(),
2084 result_var: "result".to_string(),
2085 r#async: false,
2086 path: None,
2087 method: None,
2088 args: vec![],
2089 overrides: std::collections::HashMap::new(),
2090 returns_result: true,
2091 returns_void: false,
2092 skip_languages: vec![],
2093 },
2094 ..E2eConfig::default()
2095 };
2096
2097 let fixture = make_fixture("basic_text");
2098 let resolver = FieldResolver::new(
2099 &std::collections::HashMap::new(),
2100 &std::collections::HashSet::new(),
2101 &std::collections::HashSet::new(),
2102 &std::collections::HashSet::new(),
2103 );
2104 let mut out = String::new();
2105 render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
2106
2107 assert!(
2108 out.contains("kreuzberg.CleanExtractedText("),
2109 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
2110 );
2111 assert!(
2112 !out.contains("kreuzberg.clean_extracted_text("),
2113 "must not emit raw snake_case method name, got:\n{out}"
2114 );
2115 }
2116}