1use crate::config::{CallConfig, E2eConfig};
9use crate::escape::{escape_c, sanitize_filename, sanitize_ident};
10use crate::field_access::FieldResolver;
11use crate::fixture::{Assertion, Fixture, FixtureGroup};
12use alef_core::backend::GeneratedFile;
13use alef_core::config::ResolvedCrateConfig;
14use alef_core::hash::{self, CommentStyle};
15use anyhow::Result;
16use heck::{ToPascalCase, ToSnakeCase};
17use std::collections::HashMap;
18use std::fmt::Write as FmtWrite;
19use std::path::PathBuf;
20
21use super::E2eCodegen;
22
23pub struct CCodegen;
25
26fn is_primitive_c_type(t: &str) -> bool {
30 matches!(
31 t,
32 "uint8_t"
33 | "uint16_t"
34 | "uint32_t"
35 | "uint64_t"
36 | "int8_t"
37 | "int16_t"
38 | "int32_t"
39 | "int64_t"
40 | "uintptr_t"
41 | "intptr_t"
42 | "size_t"
43 | "ssize_t"
44 | "double"
45 | "float"
46 | "bool"
47 | "int"
48 )
49}
50
51impl E2eCodegen for CCodegen {
52 fn generate(
53 &self,
54 groups: &[FixtureGroup],
55 e2e_config: &E2eConfig,
56 config: &ResolvedCrateConfig,
57 _type_defs: &[alef_core::ir::TypeDef],
58 ) -> Result<Vec<GeneratedFile>> {
59 let lang = self.language_name();
60 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
61
62 let mut files = Vec::new();
63
64 let call = &e2e_config.call;
66 let overrides = call.overrides.get(lang);
67 let result_var = &call.result_var;
68 let prefix = overrides
69 .and_then(|o| o.prefix.as_ref())
70 .cloned()
71 .or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
72 .unwrap_or_default();
73 let header = overrides
74 .and_then(|o| o.header.as_ref())
75 .cloned()
76 .unwrap_or_else(|| config.ffi_header_name());
77
78 let c_pkg = e2e_config.resolve_package("c");
80 let lib_name = c_pkg
81 .as_ref()
82 .and_then(|p| p.name.as_ref())
83 .cloned()
84 .unwrap_or_else(|| config.ffi_lib_name());
85
86 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
88 .iter()
89 .filter_map(|group| {
90 let active: Vec<&Fixture> = group
91 .fixtures
92 .iter()
93 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
94 .filter(|f| f.visitor.is_none())
95 .collect();
96 if active.is_empty() { None } else { Some((group, active)) }
97 })
98 .collect();
99
100 let ffi_crate_path = c_pkg
108 .as_ref()
109 .and_then(|p| p.path.as_ref())
110 .cloned()
111 .unwrap_or_else(|| config.ffi_crate_path());
112
113 let category_names: Vec<String> = active_groups
115 .iter()
116 .map(|(g, _)| sanitize_filename(&g.category))
117 .collect();
118 files.push(GeneratedFile {
119 path: output_base.join("Makefile"),
120 content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name),
121 generated_header: true,
122 });
123
124 let github_repo = config.github_repo();
126 let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
127 let ffi_pkg_name = e2e_config
128 .registry
129 .packages
130 .get("c")
131 .and_then(|p| p.name.as_ref())
132 .cloned()
133 .unwrap_or_else(|| lib_name.clone());
134 files.push(GeneratedFile {
135 path: output_base.join("download_ffi.sh"),
136 content: render_download_script(&github_repo, &version, &ffi_pkg_name),
137 generated_header: true,
138 });
139
140 files.push(GeneratedFile {
142 path: output_base.join("test_runner.h"),
143 content: render_test_runner_header(&active_groups),
144 generated_header: true,
145 });
146
147 files.push(GeneratedFile {
149 path: output_base.join("main.c"),
150 content: render_main_c(&active_groups),
151 generated_header: true,
152 });
153
154 let field_resolver = FieldResolver::new(
155 &e2e_config.fields,
156 &e2e_config.fields_optional,
157 &e2e_config.result_fields,
158 &e2e_config.fields_array,
159 &std::collections::HashSet::new(),
160 );
161
162 for (group, active) in &active_groups {
166 let filename = format!("test_{}.c", sanitize_filename(&group.category));
167 let content = render_test_file(
168 &group.category,
169 active,
170 &header,
171 &prefix,
172 result_var,
173 e2e_config,
174 lang,
175 &field_resolver,
176 );
177 files.push(GeneratedFile {
178 path: output_base.join(filename),
179 content,
180 generated_header: true,
181 });
182 }
183
184 Ok(files)
185 }
186
187 fn language_name(&self) -> &'static str {
188 "c"
189 }
190}
191
192struct ResolvedCallInfo {
194 function_name: String,
195 result_type_name: String,
196 options_type_name: String,
197 client_factory: Option<String>,
198 args: Vec<crate::config::ArgMapping>,
199 raw_c_result_type: Option<String>,
200 c_free_fn: Option<String>,
201 c_engine_factory: Option<String>,
202 result_is_option: bool,
203 result_is_bytes: bool,
209 extra_args: Vec<String>,
213}
214
215fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
216 let overrides = call.overrides.get(lang);
217 let function_name = overrides
218 .and_then(|o| o.function.as_ref())
219 .cloned()
220 .unwrap_or_else(|| call.function.clone());
221 let result_type_name = overrides
226 .and_then(|o| o.result_type.as_ref())
227 .cloned()
228 .unwrap_or_else(|| call.function.to_pascal_case());
229 let options_type_name = overrides
230 .and_then(|o| o.options_type.as_deref())
231 .unwrap_or("ConversionOptions")
232 .to_string();
233 let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
234 let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
235 let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
236 let c_engine_factory = overrides.and_then(|o| o.c_engine_factory.clone());
237 let result_is_option = overrides
238 .and_then(|o| if o.result_is_option { Some(true) } else { None })
239 .unwrap_or(call.result_is_option);
240 let result_is_bytes = call.result_is_bytes || overrides.is_some_and(|o| o.result_is_bytes);
245 let extra_args = overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
246 ResolvedCallInfo {
247 function_name,
248 result_type_name,
249 options_type_name,
250 client_factory,
251 args: call.args.clone(),
252 raw_c_result_type,
253 c_free_fn,
254 c_engine_factory,
255 result_is_option,
256 result_is_bytes,
257 extra_args,
258 }
259}
260
261fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
267 let call = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
268 let mut info = resolve_call_info(call, lang);
269
270 let default_overrides = e2e_config.call.overrides.get(lang);
271
272 if info.client_factory.is_none() {
275 if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
276 info.client_factory = Some(factory.clone());
277 }
278 }
279
280 if info.c_engine_factory.is_none() {
283 if let Some(factory) = default_overrides.and_then(|o| o.c_engine_factory.as_ref()) {
284 info.c_engine_factory = Some(factory.clone());
285 }
286 }
287
288 info
289}
290
291fn render_makefile(categories: &[String], header_name: &str, ffi_crate_path: &str, lib_name: &str) -> String {
292 let mut out = String::new();
293 out.push_str(&hash::header(CommentStyle::Hash));
294 let _ = writeln!(out, "CC = gcc");
295 let _ = writeln!(out, "FFI_DIR = ffi");
296 let _ = writeln!(out);
297
298 let link_lib_name = lib_name.replace('-', "_");
303
304 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
306 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
307 let _ = writeln!(
308 out,
309 " LDFLAGS = -L$(FFI_DIR)/lib -l{link_lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
310 );
311 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
312 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
313 let _ = writeln!(
314 out,
315 " LDFLAGS = -L../../target/release -l{link_lib_name} -Wl,-rpath,../../target/release"
316 );
317 let _ = writeln!(out, "else");
318 let _ = writeln!(
319 out,
320 " CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
321 );
322 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
323 let _ = writeln!(out, "endif");
324 let _ = writeln!(out);
325
326 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
327 let srcs = src_files.join(" ");
328
329 let _ = writeln!(out, "SRCS = main.c {srcs}");
330 let _ = writeln!(out, "TARGET = run_tests");
331 let _ = writeln!(out);
332 let _ = writeln!(out, ".PHONY: all clean test");
333 let _ = writeln!(out);
334 let _ = writeln!(out, "all: $(TARGET)");
335 let _ = writeln!(out);
336 let _ = writeln!(out, "$(TARGET): $(SRCS)");
337 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
338 let _ = writeln!(out);
339 let _ = writeln!(out, "MOCK_SERVER_BIN ?= ../../target/release/mock-server");
344 let _ = writeln!(out, "FIXTURES_DIR ?= ../../fixtures");
345 let _ = writeln!(out);
346 let _ = writeln!(out, "test: $(TARGET)");
347 let _ = writeln!(out, "\t@if [ -n \"$$MOCK_SERVER_URL\" ]; then \\");
348 let _ = writeln!(out, "\t\t./$(TARGET); \\");
349 let _ = writeln!(out, "\telse \\");
350 let _ = writeln!(out, "\t\tif [ ! -x \"$(MOCK_SERVER_BIN)\" ]; then \\");
351 let _ = writeln!(
352 out,
353 "\t\t\techo \"mock-server binary not found at $(MOCK_SERVER_BIN); run: cargo build -p mock-server --release\" >&2; \\"
354 );
355 let _ = writeln!(out, "\t\t\texit 1; \\");
356 let _ = writeln!(out, "\t\tfi; \\");
357 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
358 let _ = writeln!(out, "\t\tmkfifo mock_server.stdin; \\");
359 let _ = writeln!(
360 out,
361 "\t\t\"$(MOCK_SERVER_BIN)\" --fixtures \"$(FIXTURES_DIR)\" <mock_server.stdin >mock_server.stdout 2>&1 & \\"
362 );
363 let _ = writeln!(out, "\t\tMOCK_PID=$$!; \\");
364 let _ = writeln!(out, "\t\texec 9>mock_server.stdin; \\");
365 let _ = writeln!(out, "\t\tMOCK_URL=\"\"; \\");
366 let _ = writeln!(out, "\t\tfor _ in $$(seq 1 50); do \\");
367 let _ = writeln!(out, "\t\t\tif [ -s mock_server.stdout ]; then \\");
368 let _ = writeln!(
369 out,
370 "\t\t\t\tMOCK_URL=$$(grep -o 'MOCK_SERVER_URL=[^ ]*' mock_server.stdout | head -1 | cut -d= -f2); \\"
371 );
372 let _ = writeln!(out, "\t\t\t\tif [ -n \"$$MOCK_URL\" ]; then break; fi; \\");
373 let _ = writeln!(out, "\t\t\tfi; \\");
374 let _ = writeln!(out, "\t\t\tsleep 0.1; \\");
375 let _ = writeln!(out, "\t\tdone; \\");
376 let _ = writeln!(
377 out,
378 "\t\tif [ -z \"$$MOCK_URL\" ]; then echo 'failed to start mock-server' >&2; cat mock_server.stdout >&2; kill $$MOCK_PID 2>/dev/null || true; exit 1; fi; \\"
379 );
380 let _ = writeln!(out, "\t\tMOCK_SERVER_URL=\"$$MOCK_URL\" ./$(TARGET); STATUS=$$?; \\");
381 let _ = writeln!(out, "\t\texec 9>&-; \\");
382 let _ = writeln!(out, "\t\tkill $$MOCK_PID 2>/dev/null || true; \\");
383 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
384 let _ = writeln!(out, "\t\texit $$STATUS; \\");
385 let _ = writeln!(out, "\tfi");
386 let _ = writeln!(out);
387 let _ = writeln!(out, "clean:");
388 let _ = writeln!(out, "\trm -f $(TARGET) mock_server.stdout mock_server.stdin");
389 out
390}
391
392fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
393 let mut out = String::new();
394 let _ = writeln!(out, "#!/usr/bin/env bash");
395 out.push_str(&hash::header(CommentStyle::Hash));
396 let _ = writeln!(out, "set -euo pipefail");
397 let _ = writeln!(out);
398 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
399 let _ = writeln!(out, "VERSION=\"{version}\"");
400 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
401 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
402 let _ = writeln!(out);
403 let _ = writeln!(out, "# Detect OS and architecture.");
404 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
405 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
406 let _ = writeln!(out);
407 let _ = writeln!(out, "case \"$ARCH\" in");
408 let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
409 let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
410 let _ = writeln!(out, "*)");
411 let _ = writeln!(out, " echo \"Unsupported architecture: $ARCH\" >&2");
412 let _ = writeln!(out, " exit 1");
413 let _ = writeln!(out, " ;;");
414 let _ = writeln!(out, "esac");
415 let _ = writeln!(out);
416 let _ = writeln!(out, "case \"$OS\" in");
417 let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
418 let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
419 let _ = writeln!(out, "*)");
420 let _ = writeln!(out, " echo \"Unsupported OS: $OS\" >&2");
421 let _ = writeln!(out, " exit 1");
422 let _ = writeln!(out, " ;;");
423 let _ = writeln!(out, "esac");
424 let _ = writeln!(out);
425 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
426 let _ = writeln!(
427 out,
428 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
429 );
430 let _ = writeln!(out);
431 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
432 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
433 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
434 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
435 out
436}
437
438fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
439 let mut out = String::new();
440 out.push_str(&hash::header(CommentStyle::Block));
441 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
442 let _ = writeln!(out, "#define TEST_RUNNER_H");
443 let _ = writeln!(out);
444 let _ = writeln!(out, "#include <string.h>");
445 let _ = writeln!(out, "#include <stdlib.h>");
446 let _ = writeln!(out);
447 let _ = writeln!(out, "/**");
449 let _ = writeln!(
450 out,
451 " * Compare a string against an expected value, trimming trailing whitespace."
452 );
453 let _ = writeln!(
454 out,
455 " * Returns 0 if the trimmed actual string equals the expected string."
456 );
457 let _ = writeln!(out, " */");
458 let _ = writeln!(
459 out,
460 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
461 );
462 let _ = writeln!(
463 out,
464 " if (actual == NULL || expected == NULL) return actual != expected;"
465 );
466 let _ = writeln!(out, " size_t alen = strlen(actual);");
467 let _ = writeln!(
468 out,
469 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
470 );
471 let _ = writeln!(out, " size_t elen = strlen(expected);");
472 let _ = writeln!(out, " if (alen != elen) return 1;");
473 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
474 let _ = writeln!(out, "}}");
475 let _ = writeln!(out);
476
477 let _ = writeln!(out, "/**");
478 let _ = writeln!(
479 out,
480 " * Extract a string value for a given key from a JSON object string."
481 );
482 let _ = writeln!(
483 out,
484 " * Returns a heap-allocated copy of the value, or NULL if not found."
485 );
486 let _ = writeln!(out, " * Caller must free() the returned string.");
487 let _ = writeln!(out, " */");
488 let _ = writeln!(
489 out,
490 "static inline char *alef_json_get_string(const char *json, const char *key) {{"
491 );
492 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
493 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
494 let _ = writeln!(out, " size_t key_len = strlen(key);");
495 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
496 let _ = writeln!(out, " if (!pattern) return NULL;");
497 let _ = writeln!(out, " pattern[0] = '\"';");
498 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
499 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
500 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
501 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
502 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
503 let _ = writeln!(out, " free(pattern);");
504 let _ = writeln!(out, " if (!found) return NULL;");
505 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
506 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
507 let _ = writeln!(out, " if (*found != '\"') return NULL; /* not a string value */");
508 let _ = writeln!(out, " found++; /* skip opening quote */");
509 let _ = writeln!(out, " const char *end = found;");
510 let _ = writeln!(out, " while (*end && *end != '\"') {{");
511 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
512 let _ = writeln!(out, " else end++;");
513 let _ = writeln!(out, " }}");
514 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
515 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
516 let _ = writeln!(out, " if (!result_str) return NULL;");
517 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
518 let _ = writeln!(out, " result_str[val_len] = '\\0';");
519 let _ = writeln!(out, " return result_str;");
520 let _ = writeln!(out, "}}");
521 let _ = writeln!(out);
522 let _ = writeln!(out, "/**");
523 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
524 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
525 let _ = writeln!(out, " */");
526 let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
527 let _ = writeln!(out, " if (json == NULL) return 0;");
528 let _ = writeln!(out, " /* Skip leading whitespace */");
529 let _ = writeln!(
530 out,
531 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
532 );
533 let _ = writeln!(out, " if (*json != '[') return 0;");
534 let _ = writeln!(out, " json++;");
535 let _ = writeln!(out, " /* Skip whitespace after '[' */");
536 let _ = writeln!(
537 out,
538 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
539 );
540 let _ = writeln!(out, " if (*json == ']') return 0;");
541 let _ = writeln!(out, " int count = 1;");
542 let _ = writeln!(out, " int depth = 0;");
543 let _ = writeln!(out, " int in_string = 0;");
544 let _ = writeln!(
545 out,
546 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
547 );
548 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
549 let _ = writeln!(
550 out,
551 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
552 );
553 let _ = writeln!(out, " if (in_string) continue;");
554 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
555 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
556 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
557 let _ = writeln!(out, " }}");
558 let _ = writeln!(out, " return count;");
559 let _ = writeln!(out, "}}");
560 let _ = writeln!(out);
561
562 for (group, fixtures) in active_groups {
563 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
564 for fixture in fixtures {
565 let fn_name = sanitize_ident(&fixture.id);
566 let _ = writeln!(out, "void test_{fn_name}(void);");
567 }
568 let _ = writeln!(out);
569 }
570
571 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
572 out
573}
574
575fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
576 let mut out = String::new();
577 out.push_str(&hash::header(CommentStyle::Block));
578 let _ = writeln!(out, "#include <stdio.h>");
579 let _ = writeln!(out, "#include \"test_runner.h\"");
580 let _ = writeln!(out);
581 let _ = writeln!(out, "int main(void) {{");
582 let _ = writeln!(out, " int passed = 0;");
583 let _ = writeln!(out);
584
585 for (group, fixtures) in active_groups {
586 let _ = writeln!(out, " /* Category: {} */", group.category);
587 for fixture in fixtures {
588 let fn_name = sanitize_ident(&fixture.id);
589 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
590 let _ = writeln!(out, " test_{fn_name}();");
591 let _ = writeln!(out, " printf(\" PASSED\\n\");");
592 let _ = writeln!(out, " passed++;");
593 }
594 let _ = writeln!(out);
595 }
596
597 let _ = writeln!(out, " printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
598 let _ = writeln!(out, " return 0;");
599 let _ = writeln!(out, "}}");
600 out
601}
602
603#[allow(clippy::too_many_arguments)]
604fn render_test_file(
605 category: &str,
606 fixtures: &[&Fixture],
607 header: &str,
608 prefix: &str,
609 result_var: &str,
610 e2e_config: &E2eConfig,
611 lang: &str,
612 field_resolver: &FieldResolver,
613) -> String {
614 let mut out = String::new();
615 out.push_str(&hash::header(CommentStyle::Block));
616 let _ = writeln!(out, "/* E2e tests for category: {category} */");
617 let _ = writeln!(out);
618 let _ = writeln!(out, "#include <assert.h>");
619 let _ = writeln!(out, "#include <string.h>");
620 let _ = writeln!(out, "#include <stdio.h>");
621 let _ = writeln!(out, "#include <stdlib.h>");
622 let _ = writeln!(out, "#include \"{header}\"");
623 let _ = writeln!(out, "#include \"test_runner.h\"");
624 let _ = writeln!(out);
625
626 for (i, fixture) in fixtures.iter().enumerate() {
627 if fixture.visitor.is_some() {
630 panic!(
631 "C e2e generator: visitor pattern not supported for fixture: {}",
632 fixture.id
633 );
634 }
635
636 let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
637 render_test_function(
638 &mut out,
639 fixture,
640 prefix,
641 &call_info.function_name,
642 result_var,
643 &call_info.args,
644 field_resolver,
645 &e2e_config.fields_c_types,
646 &call_info.result_type_name,
647 &call_info.options_type_name,
648 call_info.client_factory.as_deref(),
649 call_info.raw_c_result_type.as_deref(),
650 call_info.c_free_fn.as_deref(),
651 call_info.c_engine_factory.as_deref(),
652 call_info.result_is_option,
653 call_info.result_is_bytes,
654 &call_info.extra_args,
655 );
656 if i + 1 < fixtures.len() {
657 let _ = writeln!(out);
658 }
659 }
660
661 out
662}
663
664#[allow(clippy::too_many_arguments)]
665fn render_test_function(
666 out: &mut String,
667 fixture: &Fixture,
668 prefix: &str,
669 function_name: &str,
670 result_var: &str,
671 args: &[crate::config::ArgMapping],
672 field_resolver: &FieldResolver,
673 fields_c_types: &HashMap<String, String>,
674 result_type_name: &str,
675 options_type_name: &str,
676 client_factory: Option<&str>,
677 raw_c_result_type: Option<&str>,
678 c_free_fn: Option<&str>,
679 c_engine_factory: Option<&str>,
680 result_is_option: bool,
681 result_is_bytes: bool,
682 extra_args: &[String],
683) {
684 let fn_name = sanitize_ident(&fixture.id);
685 let description = &fixture.description;
686
687 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
688
689 let _ = writeln!(out, "void test_{fn_name}(void) {{");
690 let _ = writeln!(out, " /* {description} */");
691
692 let prefix_upper = prefix.to_uppercase();
693
694 if let Some(config_type) = c_engine_factory {
698 render_engine_factory_test_function(
699 out,
700 fixture,
701 prefix,
702 function_name,
703 result_var,
704 field_resolver,
705 fields_c_types,
706 result_type_name,
707 config_type,
708 expects_error,
709 );
710 return;
711 }
712
713 if client_factory.is_some() && function_name == "chat_stream" {
719 render_chat_stream_test_function(out, fixture, prefix, result_var, args, options_type_name, expects_error);
720 return;
721 }
722
723 if let Some(factory) = client_factory {
731 if result_is_bytes {
732 render_bytes_test_function(
733 out,
734 fixture,
735 prefix,
736 function_name,
737 result_var,
738 args,
739 options_type_name,
740 result_type_name,
741 factory,
742 expects_error,
743 );
744 return;
745 }
746 }
747
748 if let Some(factory) = client_factory {
753 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); let mut inline_method_args: Vec<String> = Vec::new();
758
759 for arg in args {
760 if arg.arg_type == "json_object" {
761 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
766 options_type_name.to_string()
767 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
768 format!("{}Request", stripped)
769 } else {
770 format!("{result_type_name}Request")
771 };
772 let request_type_snake = request_type_pascal.to_snake_case();
773 let var_name = format!("{request_type_snake}_handle");
774
775 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
776 let json_val = if field.is_empty() || field == "input" {
777 Some(&fixture.input)
778 } else {
779 fixture.input.get(field)
780 };
781
782 if let Some(val) = json_val {
783 if !val.is_null() {
784 let normalized = super::normalize_json_keys_to_snake_case(val);
785 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
786 let escaped = escape_c(&json_str);
787 let _ = writeln!(
788 out,
789 " {prefix_upper}{request_type_pascal}* {var_name} = \
790 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
791 );
792 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
793 request_handle_vars.push((arg.name.clone(), var_name));
794 }
795 }
796 } else if arg.arg_type == "string" {
797 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
799 let val = fixture.input.get(field);
800 match val {
801 Some(v) if v.is_string() => {
802 let s = v.as_str().unwrap_or_default();
803 let escaped = escape_c(s);
804 inline_method_args.push(format!("\"{escaped}\""));
805 }
806 Some(serde_json::Value::Null) | None if arg.optional => {
807 inline_method_args.push("NULL".to_string());
808 }
809 None => {
810 inline_method_args.push("\"\"".to_string());
811 }
812 Some(other) => {
813 let s = serde_json::to_string(other).unwrap_or_default();
814 let escaped = escape_c(&s);
815 inline_method_args.push(format!("\"{escaped}\""));
816 }
817 }
818 } else if arg.optional {
819 inline_method_args.push("NULL".to_string());
821 }
822 }
823
824 let fixture_id = &fixture.id;
825 if fixture.needs_mock_server() {
826 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
827 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
828 let _ = writeln!(out, " char base_url[1024];");
829 let _ = writeln!(
830 out,
831 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
832 );
833 let _ = writeln!(
834 out,
835 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, 0, 0, NULL);"
836 );
837 } else {
838 let _ = writeln!(
839 out,
840 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
841 );
842 }
843 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
844
845 let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
846 String::new()
847 } else {
848 let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
849 let parts: Vec<String> = handles
850 .into_iter()
851 .chain(inline_method_args.iter().cloned())
852 .chain(extra_args.iter().cloned())
853 .collect();
854 format!(", {}", parts.join(", "))
855 };
856
857 let call_fn = format!("{prefix}_default_client_{function_name}");
858
859 if expects_error {
860 let _ = writeln!(
861 out,
862 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
863 );
864 for (_, var_name) in &request_handle_vars {
865 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
866 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
867 }
868 let _ = writeln!(out, " {prefix}_default_client_free(client);");
869 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
870 let _ = writeln!(out, "}}");
871 return;
872 }
873
874 let _ = writeln!(
875 out,
876 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
877 );
878 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
879
880 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
881 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
882 let mut primitive_locals: HashMap<String, String> = HashMap::new();
885
886 for assertion in &fixture.assertions {
887 if let Some(f) = &assertion.field {
888 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
889 let resolved = field_resolver.resolve(f);
890 let local_var = f.replace(['.', '['], "_").replace(']', "");
891 let has_map_access = resolved.contains('[');
892 if resolved.contains('.') {
893 let leaf_primitive = emit_nested_accessor(
894 out,
895 prefix,
896 resolved,
897 &local_var,
898 result_var,
899 fields_c_types,
900 &mut intermediate_handles,
901 result_type_name,
902 );
903 if let Some(prim) = leaf_primitive {
904 primitive_locals.insert(local_var.clone(), prim);
905 }
906 } else {
907 let result_type_snake = result_type_name.to_snake_case();
908 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
909 let lookup_key = format!("{result_type_snake}.{resolved}");
910 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
911 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
912 primitive_locals.insert(local_var.clone(), t.clone());
913 } else {
914 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
915 }
916 }
917 accessed_fields.push((f.clone(), local_var, has_map_access));
918 }
919 }
920 }
921
922 for assertion in &fixture.assertions {
923 render_assertion(
924 out,
925 assertion,
926 result_var,
927 prefix,
928 field_resolver,
929 &accessed_fields,
930 &primitive_locals,
931 );
932 }
933
934 for (_f, local_var, from_json) in &accessed_fields {
935 if primitive_locals.contains_key(local_var) {
936 continue;
937 }
938 if *from_json {
939 let _ = writeln!(out, " free({local_var});");
940 } else {
941 let _ = writeln!(out, " {prefix}_free_string({local_var});");
942 }
943 }
944 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
945 if snake_type == "free_string" {
946 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
947 } else {
948 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
949 }
950 }
951 let result_type_snake = result_type_name.to_snake_case();
952 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
953 for (_, var_name) in &request_handle_vars {
954 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
955 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
956 }
957 let _ = writeln!(out, " {prefix}_default_client_free(client);");
958 let _ = writeln!(out, "}}");
959 return;
960 }
961
962 if let Some(raw_type) = raw_c_result_type {
965 let args_str = if args.is_empty() {
967 String::new()
968 } else {
969 let parts: Vec<String> = args
970 .iter()
971 .filter_map(|arg| {
972 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
973 let val = fixture.input.get(field);
974 match val {
975 None if arg.optional => Some("NULL".to_string()),
976 None => None,
977 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
978 Some(v) => Some(json_to_c(v)),
979 }
980 })
981 .collect();
982 parts.join(", ")
983 };
984
985 let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
987
988 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
990 if has_not_error {
991 match raw_type {
992 "char*" if !result_is_option => {
993 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
994 }
995 "int32_t" => {
996 let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
997 }
998 "uintptr_t" => {
999 let _ = writeln!(
1000 out,
1001 " assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
1002 );
1003 }
1004 _ => {}
1005 }
1006 }
1007
1008 for assertion in &fixture.assertions {
1010 match assertion.assertion_type.as_str() {
1011 "not_error" | "error" => {} "not_empty" => {
1013 let _ = writeln!(
1014 out,
1015 " assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
1016 );
1017 }
1018 "is_empty" => {
1019 if result_is_option && raw_type == "char*" {
1020 let _ = writeln!(
1021 out,
1022 " assert({result_var} == NULL && \"expected empty/null value\");"
1023 );
1024 } else {
1025 let _ = writeln!(
1026 out,
1027 " assert(strlen({result_var}) == 0 && \"expected empty value\");"
1028 );
1029 }
1030 }
1031 "count_min" => {
1032 if let Some(val) = &assertion.value {
1033 if let Some(n) = val.as_u64() {
1034 match raw_type {
1035 "char*" => {
1036 let _ = writeln!(out, " {{");
1037 let _ = writeln!(
1038 out,
1039 " assert({result_var} != NULL && \"expected non-null JSON array\");"
1040 );
1041 let _ =
1042 writeln!(out, " int elem_count = alef_json_array_count({result_var});");
1043 let _ = writeln!(
1044 out,
1045 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1046 );
1047 let _ = writeln!(out, " }}");
1048 }
1049 _ => {
1050 let _ = writeln!(
1051 out,
1052 " assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
1053 );
1054 }
1055 }
1056 }
1057 }
1058 }
1059 "greater_than_or_equal" => {
1060 if let Some(val) = &assertion.value {
1061 let c_val = json_to_c(val);
1062 let _ = writeln!(
1063 out,
1064 " assert({result_var} >= {c_val} && \"expected greater than or equal\");"
1065 );
1066 }
1067 }
1068 "contains" => {
1069 if let Some(val) = &assertion.value {
1070 let c_val = json_to_c(val);
1071 let _ = writeln!(
1072 out,
1073 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1074 );
1075 }
1076 }
1077 "contains_all" => {
1078 if let Some(values) = &assertion.values {
1079 for val in values {
1080 let c_val = json_to_c(val);
1081 let _ = writeln!(
1082 out,
1083 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1084 );
1085 }
1086 }
1087 }
1088 "equals" => {
1089 if let Some(val) = &assertion.value {
1090 let c_val = json_to_c(val);
1091 if val.is_string() {
1092 let _ = writeln!(
1093 out,
1094 " assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
1095 );
1096 } else {
1097 let _ = writeln!(
1098 out,
1099 " assert({result_var} == {c_val} && \"equals assertion failed\");"
1100 );
1101 }
1102 }
1103 }
1104 "not_contains" => {
1105 if let Some(val) = &assertion.value {
1106 let c_val = json_to_c(val);
1107 let _ = writeln!(
1108 out,
1109 " assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1110 );
1111 }
1112 }
1113 "starts_with" => {
1114 if let Some(val) = &assertion.value {
1115 let c_val = json_to_c(val);
1116 let _ = writeln!(
1117 out,
1118 " assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1119 );
1120 }
1121 }
1122 "is_true" => {
1123 let _ = writeln!(out, " assert({result_var});");
1124 }
1125 "is_false" => {
1126 let _ = writeln!(out, " assert(!{result_var});");
1127 }
1128 other => {
1129 panic!("C e2e raw-result generator: unsupported assertion type: {other}");
1130 }
1131 }
1132 }
1133
1134 if raw_type == "char*" {
1136 let free_fn = c_free_fn
1137 .map(|s| s.to_string())
1138 .unwrap_or_else(|| format!("{prefix}_free_string"));
1139 if result_is_option {
1140 let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
1141 } else {
1142 let _ = writeln!(out, " {free_fn}({result_var});");
1143 }
1144 }
1145
1146 let _ = writeln!(out, "}}");
1147 return;
1148 }
1149
1150 let prefixed_fn = function_name.to_string();
1156
1157 let mut has_options_handle = false;
1159 for arg in args {
1160 if arg.arg_type == "json_object" {
1161 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1162 if let Some(val) = fixture.input.get(field) {
1163 if !val.is_null() {
1164 let normalized = super::normalize_json_keys_to_snake_case(val);
1168 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1169 let escaped = escape_c(&json_str);
1170 let upper = prefix.to_uppercase();
1171 let options_type_pascal = options_type_name;
1172 let options_type_snake = options_type_name.to_snake_case();
1173 let _ = writeln!(
1174 out,
1175 " {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
1176 );
1177 has_options_handle = true;
1178 }
1179 }
1180 }
1181 }
1182
1183 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
1184
1185 if expects_error {
1186 let _ = writeln!(
1187 out,
1188 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1189 );
1190 if has_options_handle {
1191 let options_type_snake = options_type_name.to_snake_case();
1192 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1193 }
1194 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1195 let _ = writeln!(out, "}}");
1196 return;
1197 }
1198
1199 let _ = writeln!(
1201 out,
1202 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1203 );
1204 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1205
1206 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1214 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1217 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1219
1220 for assertion in &fixture.assertions {
1221 if let Some(f) = &assertion.field {
1222 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1223 let resolved = field_resolver.resolve(f);
1224 let local_var = f.replace(['.', '['], "_").replace(']', "");
1225 let has_map_access = resolved.contains('[');
1226
1227 if resolved.contains('.') {
1228 let leaf_primitive = emit_nested_accessor(
1229 out,
1230 prefix,
1231 resolved,
1232 &local_var,
1233 result_var,
1234 fields_c_types,
1235 &mut intermediate_handles,
1236 result_type_name,
1237 );
1238 if let Some(prim) = leaf_primitive {
1239 primitive_locals.insert(local_var.clone(), prim);
1240 }
1241 } else {
1242 let result_type_snake = result_type_name.to_snake_case();
1243 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1244 let lookup_key = format!("{result_type_snake}.{resolved}");
1245 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1246 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1247 primitive_locals.insert(local_var.clone(), t.clone());
1248 } else {
1249 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1250 }
1251 }
1252 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1253 }
1254 }
1255 }
1256
1257 for assertion in &fixture.assertions {
1258 render_assertion(
1259 out,
1260 assertion,
1261 result_var,
1262 prefix,
1263 field_resolver,
1264 &accessed_fields,
1265 &primitive_locals,
1266 );
1267 }
1268
1269 for (_f, local_var, from_json) in &accessed_fields {
1271 if primitive_locals.contains_key(local_var) {
1272 continue;
1273 }
1274 if *from_json {
1275 let _ = writeln!(out, " free({local_var});");
1276 } else {
1277 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1278 }
1279 }
1280 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1282 if snake_type == "free_string" {
1283 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1285 } else {
1286 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1287 }
1288 }
1289 if has_options_handle {
1290 let options_type_snake = options_type_name.to_snake_case();
1291 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1292 }
1293 let result_type_snake = result_type_name.to_snake_case();
1294 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1295 let _ = writeln!(out, "}}");
1296}
1297
1298#[allow(clippy::too_many_arguments)]
1307fn render_engine_factory_test_function(
1308 out: &mut String,
1309 fixture: &Fixture,
1310 prefix: &str,
1311 function_name: &str,
1312 result_var: &str,
1313 field_resolver: &FieldResolver,
1314 fields_c_types: &HashMap<String, String>,
1315 result_type_name: &str,
1316 config_type: &str,
1317 _expects_error: bool,
1318) {
1319 let prefix_upper = prefix.to_uppercase();
1320 let config_snake = config_type.to_snake_case();
1321
1322 let config_val = fixture.input.get("config");
1324 let config_json = match config_val {
1325 Some(v) if !v.is_null() => {
1326 let normalized = super::normalize_json_keys_to_snake_case(v);
1327 serde_json::to_string(&normalized).unwrap_or_else(|_| "{}".to_string())
1328 }
1329 _ => "{}".to_string(),
1330 };
1331 let config_escaped = escape_c(&config_json);
1332 let fixture_id = &fixture.id;
1333
1334 let has_active_assertions = fixture.assertions.iter().any(|a| {
1338 if let Some(f) = &a.field {
1339 !f.is_empty() && field_resolver.is_valid_for_result(f)
1340 } else {
1341 false
1342 }
1343 });
1344
1345 let _ = writeln!(
1347 out,
1348 " {prefix_upper}{config_type}* config_handle = \
1349 {prefix}_{config_snake}_from_json(\"{config_escaped}\");"
1350 );
1351 let _ = writeln!(out, " assert(config_handle != NULL && \"failed to parse config\");");
1352 let _ = writeln!(
1353 out,
1354 " {prefix_upper}CrawlEngineHandle* engine = {prefix}_create_engine(config_handle);"
1355 );
1356 let _ = writeln!(out, " {prefix}_{config_snake}_free(config_handle);");
1357 let _ = writeln!(out, " assert(engine != NULL && \"failed to create engine\");");
1358
1359 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1361 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1362 let _ = writeln!(out, " char url[2048];");
1363 let _ = writeln!(
1364 out,
1365 " snprintf(url, sizeof(url), \"%s/fixtures/{fixture_id}\", mock_base);"
1366 );
1367
1368 let _ = writeln!(
1370 out,
1371 " {prefix_upper}{result_type_name}* {result_var} = {prefix}_{function_name}(engine, url);"
1372 );
1373
1374 if !has_active_assertions {
1377 let result_type_snake = result_type_name.to_snake_case();
1378 let _ = writeln!(
1379 out,
1380 " if ({result_var} != NULL) {prefix}_{result_type_snake}_free({result_var});"
1381 );
1382 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
1383 let _ = writeln!(out, "}}");
1384 return;
1385 }
1386
1387 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1388
1389 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1391 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1392 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1393
1394 for assertion in &fixture.assertions {
1395 if let Some(f) = &assertion.field {
1396 if !f.is_empty() && field_resolver.is_valid_for_result(f) && !accessed_fields.iter().any(|(k, _, _)| k == f)
1397 {
1398 let resolved = field_resolver.resolve(f);
1399 let local_var = f.replace(['.', '['], "_").replace(']', "");
1400 let has_map_access = resolved.contains('[');
1401 if resolved.contains('.') {
1402 let leaf_primitive = emit_nested_accessor(
1403 out,
1404 prefix,
1405 resolved,
1406 &local_var,
1407 result_var,
1408 fields_c_types,
1409 &mut intermediate_handles,
1410 result_type_name,
1411 );
1412 if let Some(prim) = leaf_primitive {
1413 primitive_locals.insert(local_var.clone(), prim);
1414 }
1415 } else {
1416 let result_type_snake = result_type_name.to_snake_case();
1417 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1418 let lookup_key = format!("{result_type_snake}.{resolved}");
1419 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1420 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1421 primitive_locals.insert(local_var.clone(), t.clone());
1422 } else {
1423 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1424 }
1425 }
1426 accessed_fields.push((f.clone(), local_var, has_map_access));
1427 }
1428 }
1429 }
1430
1431 for assertion in &fixture.assertions {
1432 render_assertion(
1433 out,
1434 assertion,
1435 result_var,
1436 prefix,
1437 field_resolver,
1438 &accessed_fields,
1439 &primitive_locals,
1440 );
1441 }
1442
1443 for (_f, local_var, from_json) in &accessed_fields {
1445 if primitive_locals.contains_key(local_var) {
1446 continue;
1447 }
1448 if *from_json {
1449 let _ = writeln!(out, " free({local_var});");
1450 } else {
1451 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1452 }
1453 }
1454 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1455 if snake_type == "free_string" {
1456 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1457 } else {
1458 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1459 }
1460 }
1461
1462 let result_type_snake = result_type_name.to_snake_case();
1463 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1464 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
1465 let _ = writeln!(out, "}}");
1466}
1467
1468#[allow(clippy::too_many_arguments)]
1490fn render_bytes_test_function(
1491 out: &mut String,
1492 fixture: &Fixture,
1493 prefix: &str,
1494 function_name: &str,
1495 _result_var: &str,
1496 args: &[crate::config::ArgMapping],
1497 options_type_name: &str,
1498 result_type_name: &str,
1499 factory: &str,
1500 expects_error: bool,
1501) {
1502 let prefix_upper = prefix.to_uppercase();
1503 let mut request_handle_vars: Vec<(String, String)> = Vec::new();
1504 let mut string_arg_exprs: Vec<String> = Vec::new();
1505
1506 for arg in args {
1507 match arg.arg_type.as_str() {
1508 "json_object" => {
1509 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1510 options_type_name.to_string()
1511 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1512 format!("{}Request", stripped)
1513 } else {
1514 format!("{result_type_name}Request")
1515 };
1516 let request_type_snake = request_type_pascal.to_snake_case();
1517 let var_name = format!("{request_type_snake}_handle");
1518
1519 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1520 let json_val = if field.is_empty() || field == "input" {
1521 Some(&fixture.input)
1522 } else {
1523 fixture.input.get(field)
1524 };
1525
1526 if let Some(val) = json_val {
1527 if !val.is_null() {
1528 let normalized = super::normalize_json_keys_to_snake_case(val);
1529 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1530 let escaped = escape_c(&json_str);
1531 let _ = writeln!(
1532 out,
1533 " {prefix_upper}{request_type_pascal}* {var_name} = \
1534 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1535 );
1536 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1537 request_handle_vars.push((arg.name.clone(), var_name));
1538 }
1539 }
1540 }
1541 "string" => {
1542 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1545 let val = fixture.input.get(field);
1546 let expr = match val {
1547 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
1548 Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
1549 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
1550 None => "NULL".to_string(),
1551 };
1552 string_arg_exprs.push(expr);
1553 }
1554 _ => {
1555 string_arg_exprs.push("NULL".to_string());
1558 }
1559 }
1560 }
1561
1562 let fixture_id = &fixture.id;
1563 if fixture.needs_mock_server() {
1564 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1565 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1566 let _ = writeln!(out, " char base_url[1024];");
1567 let _ = writeln!(
1568 out,
1569 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1570 );
1571 let _ = writeln!(
1572 out,
1573 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, 0, 0, NULL);"
1574 );
1575 } else {
1576 let _ = writeln!(
1577 out,
1578 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
1579 );
1580 }
1581 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1582
1583 let _ = writeln!(out, " uint8_t* out_ptr = NULL;");
1585 let _ = writeln!(out, " uintptr_t out_len = 0;");
1586 let _ = writeln!(out, " uintptr_t out_cap = 0;");
1587
1588 let mut method_args: Vec<String> = Vec::new();
1590 for (_, v) in &request_handle_vars {
1591 method_args.push(v.clone());
1592 }
1593 method_args.extend(string_arg_exprs.iter().cloned());
1594 let extra_args = if method_args.is_empty() {
1595 String::new()
1596 } else {
1597 format!(", {}", method_args.join(", "))
1598 };
1599
1600 let call_fn = format!("{prefix}_default_client_{function_name}");
1601 let _ = writeln!(
1602 out,
1603 " int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
1604 );
1605
1606 if expects_error {
1607 for (_, var_name) in &request_handle_vars {
1608 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1609 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1610 }
1611 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1612 let _ = writeln!(out, " assert(status != 0 && \"expected call to fail\");");
1613 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
1616 let _ = writeln!(out, "}}");
1617 return;
1618 }
1619
1620 let _ = writeln!(out, " assert(status == 0 && \"expected call to succeed\");");
1621
1622 let mut emitted_len_check = false;
1627 for assertion in &fixture.assertions {
1628 match assertion.assertion_type.as_str() {
1629 "not_error" => {
1630 }
1632 "not_empty" | "not_null" => {
1633 if !emitted_len_check {
1634 let _ = writeln!(out, " assert(out_len > 0 && \"expected non-empty value\");");
1635 emitted_len_check = true;
1636 }
1637 }
1638 _ => {
1639 let _ = writeln!(
1643 out,
1644 " /* skipped: assertion '{}' not meaningful on raw byte buffer */",
1645 assertion.assertion_type
1646 );
1647 }
1648 }
1649 }
1650
1651 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
1652 for (_, var_name) in &request_handle_vars {
1653 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1654 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1655 }
1656 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1657 let _ = writeln!(out, "}}");
1658}
1659
1660fn render_chat_stream_test_function(
1671 out: &mut String,
1672 fixture: &Fixture,
1673 prefix: &str,
1674 result_var: &str,
1675 args: &[crate::config::ArgMapping],
1676 options_type_name: &str,
1677 expects_error: bool,
1678) {
1679 let prefix_upper = prefix.to_uppercase();
1680
1681 let mut request_var: Option<String> = None;
1682 for arg in args {
1683 if arg.arg_type == "json_object" {
1684 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1685 options_type_name.to_string()
1686 } else {
1687 "ChatCompletionRequest".to_string()
1688 };
1689 let request_type_snake = request_type_pascal.to_snake_case();
1690 let var_name = format!("{request_type_snake}_handle");
1691
1692 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1693 let json_val = if field.is_empty() || field == "input" {
1694 Some(&fixture.input)
1695 } else {
1696 fixture.input.get(field)
1697 };
1698
1699 if let Some(val) = json_val {
1700 if !val.is_null() {
1701 let normalized = super::normalize_json_keys_to_snake_case(val);
1702 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1703 let escaped = escape_c(&json_str);
1704 let _ = writeln!(
1705 out,
1706 " {prefix_upper}{request_type_pascal}* {var_name} = \
1707 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1708 );
1709 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1710 request_var = Some(var_name);
1711 break;
1712 }
1713 }
1714 }
1715 }
1716
1717 let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
1718 let req_snake = request_var
1719 .as_ref()
1720 .and_then(|v| v.strip_suffix("_handle"))
1721 .unwrap_or("chat_completion_request")
1722 .to_string();
1723
1724 let fixture_id = &fixture.id;
1725 if fixture.needs_mock_server() {
1726 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1727 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1728 let _ = writeln!(out, " char base_url[1024];");
1729 let _ = writeln!(
1730 out,
1731 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1732 );
1733 let _ = writeln!(
1734 out,
1735 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, 0, 0, NULL);"
1736 );
1737 } else {
1738 let _ = writeln!(
1739 out,
1740 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, 0, 0, NULL);"
1741 );
1742 }
1743 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1744
1745 let _ = writeln!(
1746 out,
1747 " {prefix_upper}LiterllmDefaultClientChatStreamStreamHandle* stream_handle = \
1748 {prefix}_default_client_chat_stream_start(client, {req_handle});"
1749 );
1750
1751 if expects_error {
1752 let _ = writeln!(
1753 out,
1754 " assert(stream_handle == NULL && \"expected stream-start to fail\");"
1755 );
1756 if request_var.is_some() {
1757 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
1758 }
1759 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1760 let _ = writeln!(out, "}}");
1761 return;
1762 }
1763
1764 let _ = writeln!(
1765 out,
1766 " assert(stream_handle != NULL && \"expected stream-start to succeed\");"
1767 );
1768
1769 let _ = writeln!(out, " size_t chunks_count = 0;");
1770 let _ = writeln!(out, " char* stream_content = (char*)malloc(1);");
1771 let _ = writeln!(out, " assert(stream_content != NULL);");
1772 let _ = writeln!(out, " stream_content[0] = '\\0';");
1773 let _ = writeln!(out, " size_t stream_content_len = 0;");
1774 let _ = writeln!(out, " int stream_complete = 0;");
1775 let _ = writeln!(out, " int no_chunks_after_done = 1;");
1776 let _ = writeln!(out, " char* last_choices_json = NULL;");
1777 let _ = writeln!(out, " uint64_t total_tokens = 0;");
1778 let _ = writeln!(out);
1779
1780 let _ = writeln!(out, " while (1) {{");
1781 let _ = writeln!(
1782 out,
1783 " {prefix_upper}ChatCompletionChunk* {result_var} = \
1784 {prefix}_default_client_chat_stream_next(stream_handle);"
1785 );
1786 let _ = writeln!(out, " if ({result_var} == NULL) {{");
1787 let _ = writeln!(
1788 out,
1789 " if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
1790 );
1791 let _ = writeln!(out, " break;");
1792 let _ = writeln!(out, " }}");
1793 let _ = writeln!(out, " chunks_count++;");
1794 let _ = writeln!(
1795 out,
1796 " char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
1797 );
1798 let _ = writeln!(out, " if (choices_json != NULL) {{");
1799 let _ = writeln!(
1800 out,
1801 " const char* d = strstr(choices_json, \"\\\"content\\\":\");"
1802 );
1803 let _ = writeln!(out, " if (d != NULL) {{");
1804 let _ = writeln!(out, " d += 10;");
1805 let _ = writeln!(out, " while (*d == ' ' || *d == '\\t') d++;");
1806 let _ = writeln!(out, " if (*d == '\"') {{");
1807 let _ = writeln!(out, " d++;");
1808 let _ = writeln!(out, " const char* e = d;");
1809 let _ = writeln!(out, " while (*e && *e != '\"') {{");
1810 let _ = writeln!(
1811 out,
1812 " if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
1813 );
1814 let _ = writeln!(out, " }}");
1815 let _ = writeln!(out, " size_t add = (size_t)(e - d);");
1816 let _ = writeln!(out, " if (add > 0) {{");
1817 let _ = writeln!(
1818 out,
1819 " char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
1820 );
1821 let _ = writeln!(out, " if (nc != NULL) {{");
1822 let _ = writeln!(out, " stream_content = nc;");
1823 let _ = writeln!(
1824 out,
1825 " memcpy(stream_content + stream_content_len, d, add);"
1826 );
1827 let _ = writeln!(out, " stream_content_len += add;");
1828 let _ = writeln!(
1829 out,
1830 " stream_content[stream_content_len] = '\\0';"
1831 );
1832 let _ = writeln!(out, " }}");
1833 let _ = writeln!(out, " }}");
1834 let _ = writeln!(out, " }}");
1835 let _ = writeln!(out, " }}");
1836 let _ = writeln!(
1837 out,
1838 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
1839 );
1840 let _ = writeln!(out, " last_choices_json = choices_json;");
1841 let _ = writeln!(out, " }}");
1842 let _ = writeln!(
1843 out,
1844 " {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
1845 );
1846 let _ = writeln!(out, " if (usage_handle != NULL) {{");
1847 let _ = writeln!(
1848 out,
1849 " total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
1850 );
1851 let _ = writeln!(out, " {prefix}_usage_free(usage_handle);");
1852 let _ = writeln!(out, " }}");
1853 let _ = writeln!(out, " {prefix}_chat_completion_chunk_free({result_var});");
1854 let _ = writeln!(out, " }}");
1855 let _ = writeln!(out, " {prefix}_default_client_chat_stream_free(stream_handle);");
1856 let _ = writeln!(out);
1857
1858 let _ = writeln!(out, " char* finish_reason = NULL;");
1859 let _ = writeln!(out, " char* tool_calls_json = NULL;");
1860 let _ = writeln!(out, " char* tool_calls_0_function_name = NULL;");
1861 let _ = writeln!(out, " if (last_choices_json != NULL) {{");
1862 let _ = writeln!(
1863 out,
1864 " finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
1865 );
1866 let _ = writeln!(
1867 out,
1868 " const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
1869 );
1870 let _ = writeln!(out, " if (tc != NULL) {{");
1871 let _ = writeln!(out, " tc += 13;");
1872 let _ = writeln!(out, " while (*tc == ' ' || *tc == '\\t') tc++;");
1873 let _ = writeln!(out, " if (*tc == '[') {{");
1874 let _ = writeln!(out, " int depth = 0;");
1875 let _ = writeln!(out, " const char* end = tc;");
1876 let _ = writeln!(out, " int in_str = 0;");
1877 let _ = writeln!(out, " for (; *end; end++) {{");
1878 let _ = writeln!(
1879 out,
1880 " if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
1881 );
1882 let _ = writeln!(
1883 out,
1884 " if (*end == '\"') {{ in_str = !in_str; continue; }}"
1885 );
1886 let _ = writeln!(out, " if (in_str) continue;");
1887 let _ = writeln!(out, " if (*end == '[' || *end == '{{') depth++;");
1888 let _ = writeln!(
1889 out,
1890 " else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
1891 );
1892 let _ = writeln!(out, " }}");
1893 let _ = writeln!(out, " size_t tlen = (size_t)(end - tc);");
1894 let _ = writeln!(out, " tool_calls_json = (char*)malloc(tlen + 1);");
1895 let _ = writeln!(out, " if (tool_calls_json != NULL) {{");
1896 let _ = writeln!(out, " memcpy(tool_calls_json, tc, tlen);");
1897 let _ = writeln!(out, " tool_calls_json[tlen] = '\\0';");
1898 let _ = writeln!(
1899 out,
1900 " const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
1901 );
1902 let _ = writeln!(out, " if (fn != NULL) {{");
1903 let _ = writeln!(
1904 out,
1905 " const char* np = strstr(fn, \"\\\"name\\\":\");"
1906 );
1907 let _ = writeln!(out, " if (np != NULL) {{");
1908 let _ = writeln!(out, " np += 7;");
1909 let _ = writeln!(
1910 out,
1911 " while (*np == ' ' || *np == '\\t') np++;"
1912 );
1913 let _ = writeln!(out, " if (*np == '\"') {{");
1914 let _ = writeln!(out, " np++;");
1915 let _ = writeln!(out, " const char* ne = np;");
1916 let _ = writeln!(
1917 out,
1918 " while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
1919 );
1920 let _ = writeln!(out, " size_t nlen = (size_t)(ne - np);");
1921 let _ = writeln!(
1922 out,
1923 " tool_calls_0_function_name = (char*)malloc(nlen + 1);"
1924 );
1925 let _ = writeln!(
1926 out,
1927 " if (tool_calls_0_function_name != NULL) {{"
1928 );
1929 let _ = writeln!(
1930 out,
1931 " memcpy(tool_calls_0_function_name, np, nlen);"
1932 );
1933 let _ = writeln!(
1934 out,
1935 " tool_calls_0_function_name[nlen] = '\\0';"
1936 );
1937 let _ = writeln!(out, " }}");
1938 let _ = writeln!(out, " }}");
1939 let _ = writeln!(out, " }}");
1940 let _ = writeln!(out, " }}");
1941 let _ = writeln!(out, " }}");
1942 let _ = writeln!(out, " }}");
1943 let _ = writeln!(out, " }}");
1944 let _ = writeln!(out, " }}");
1945 let _ = writeln!(out);
1946
1947 for assertion in &fixture.assertions {
1948 emit_chat_stream_assertion(out, assertion);
1949 }
1950
1951 let _ = writeln!(out, " free(stream_content);");
1952 let _ = writeln!(
1953 out,
1954 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
1955 );
1956 let _ = writeln!(out, " if (finish_reason != NULL) free(finish_reason);");
1957 let _ = writeln!(out, " if (tool_calls_json != NULL) free(tool_calls_json);");
1958 let _ = writeln!(
1959 out,
1960 " if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
1961 );
1962 if request_var.is_some() {
1963 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
1964 }
1965 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1966 let _ = writeln!(
1967 out,
1968 " /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
1969 (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
1970 );
1971 let _ = writeln!(out, "}}");
1972}
1973
1974fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
1978 let field = assertion.field.as_deref().unwrap_or("");
1979
1980 enum Kind {
1981 IntCount,
1982 Bool,
1983 Str,
1984 IntTokens,
1985 Unsupported,
1986 }
1987
1988 let (expr, kind) = match field {
1989 "chunks" => ("chunks_count", Kind::IntCount),
1990 "stream_content" => ("stream_content", Kind::Str),
1991 "stream_complete" => ("stream_complete", Kind::Bool),
1992 "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
1993 "finish_reason" => ("finish_reason", Kind::Str),
1994 "tool_calls" => ("tool_calls_json", Kind::Str),
1995 "tool_calls[0].function.name" => ("tool_calls_0_function_name", Kind::Str),
1996 "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
1997 _ => ("", Kind::Unsupported),
1998 };
1999
2000 let atype = assertion.assertion_type.as_str();
2001 if atype == "not_error" || atype == "error" {
2002 return;
2003 }
2004
2005 if matches!(kind, Kind::Unsupported) {
2006 let _ = writeln!(
2007 out,
2008 " /* skipped: streaming assertion on unsupported field '{field}' */"
2009 );
2010 return;
2011 }
2012
2013 match (atype, &kind) {
2014 ("count_min", Kind::IntCount) => {
2015 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2016 let _ = writeln!(out, " assert({expr} >= {n} && \"expected at least {n} chunks\");");
2017 }
2018 }
2019 ("equals", Kind::Str) => {
2020 if let Some(val) = &assertion.value {
2021 let c_val = json_to_c(val);
2022 let _ = writeln!(
2023 out,
2024 " assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
2025 );
2026 }
2027 }
2028 ("contains", Kind::Str) => {
2029 if let Some(val) = &assertion.value {
2030 let c_val = json_to_c(val);
2031 let _ = writeln!(
2032 out,
2033 " assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
2034 );
2035 }
2036 }
2037 ("not_empty", Kind::Str) => {
2038 let _ = writeln!(
2039 out,
2040 " assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
2041 );
2042 }
2043 ("is_true", Kind::Bool) => {
2044 let _ = writeln!(out, " assert({expr} && \"expected {field} to be true\");");
2045 }
2046 ("is_false", Kind::Bool) => {
2047 let _ = writeln!(out, " assert(!{expr} && \"expected {field} to be false\");");
2048 }
2049 ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
2050 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2051 let _ = writeln!(out, " assert({expr} >= {n} && \"expected {expr} >= {n}\");");
2052 }
2053 }
2054 ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
2055 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2056 let _ = writeln!(out, " assert({expr} == {n} && \"equals assertion failed\");");
2057 }
2058 }
2059 _ => {
2060 let _ = writeln!(
2061 out,
2062 " /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
2063 );
2064 }
2065 }
2066}
2067
2068#[allow(clippy::too_many_arguments)]
2082fn emit_nested_accessor(
2083 out: &mut String,
2084 prefix: &str,
2085 resolved: &str,
2086 local_var: &str,
2087 result_var: &str,
2088 fields_c_types: &HashMap<String, String>,
2089 intermediate_handles: &mut Vec<(String, String)>,
2090 result_type_name: &str,
2091) -> Option<String> {
2092 let segments: Vec<&str> = resolved.split('.').collect();
2093 let prefix_upper = prefix.to_uppercase();
2094
2095 let mut current_snake_type = result_type_name.to_snake_case();
2097 let mut current_handle = result_var.to_string();
2098 let mut json_extract_mode = false;
2101
2102 for (i, segment) in segments.iter().enumerate() {
2103 let is_leaf = i + 1 == segments.len();
2104
2105 if json_extract_mode {
2108 let seg_snake = segment.to_snake_case();
2109 if is_leaf {
2110 let _ = writeln!(
2111 out,
2112 " char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2113 );
2114 return None; }
2116 let json_var = format!("{seg_snake}_json");
2118 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2119 let _ = writeln!(
2120 out,
2121 " char* {json_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2122 );
2123 intermediate_handles.push((json_var.clone(), "free".to_string()));
2124 }
2125 current_handle = json_var;
2126 continue;
2127 }
2128
2129 if let Some(bracket_pos) = segment.find('[') {
2131 let field_name = &segment[..bracket_pos];
2132 let key = segment[bracket_pos + 1..].trim_end_matches(']');
2133 let field_snake = field_name.to_snake_case();
2134 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
2135
2136 let json_var = format!("{field_snake}_json");
2138 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2139 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2140 let _ = writeln!(out, " assert({json_var} != NULL);");
2141 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2143 }
2144
2145 if key.is_empty() {
2146 current_handle = json_var;
2149 json_extract_mode = true;
2150 continue;
2151 }
2152
2153 let _ = writeln!(
2155 out,
2156 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
2157 );
2158 return None; }
2160
2161 let seg_snake = segment.to_snake_case();
2162 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
2163
2164 if is_leaf {
2165 let lookup_key = format!("{current_snake_type}.{seg_snake}");
2168 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
2169 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({current_handle});");
2170 return Some(t.clone());
2171 }
2172 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
2173 } else {
2174 let lookup_key = format!("{current_snake_type}.{seg_snake}");
2176 let return_type_pascal = match fields_c_types.get(&lookup_key) {
2177 Some(t) => t.clone(),
2178 None => {
2179 segment.to_pascal_case()
2181 }
2182 };
2183
2184 if return_type_pascal == "char*" {
2187 let json_var = format!("{seg_snake}_json");
2188 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2189 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2190 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2191 }
2192 if i + 2 == segments.len() && segments[i + 1] == "length" {
2194 let _ = writeln!(out, " int {local_var} = alef_json_array_count({json_var});");
2195 return Some("int".to_string());
2196 }
2197 current_snake_type = seg_snake.clone();
2198 current_handle = json_var;
2199 continue;
2200 }
2201
2202 let return_snake = return_type_pascal.to_snake_case();
2203 let handle_var = format!("{seg_snake}_handle");
2204
2205 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
2208 let _ = writeln!(
2209 out,
2210 " {prefix_upper}{return_type_pascal}* {handle_var} = \
2211 {accessor_fn}({current_handle});"
2212 );
2213 let _ = writeln!(out, " assert({handle_var} != NULL);");
2214 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
2215 }
2216
2217 current_snake_type = return_snake;
2218 current_handle = handle_var;
2219 }
2220 }
2221 None
2222}
2223
2224fn build_args_string_c(
2228 input: &serde_json::Value,
2229 args: &[crate::config::ArgMapping],
2230 has_options_handle: bool,
2231) -> String {
2232 if args.is_empty() {
2233 return json_to_c(input);
2234 }
2235
2236 let parts: Vec<String> = args
2237 .iter()
2238 .filter_map(|arg| {
2239 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2240 let val = input.get(field);
2241 match val {
2242 None if arg.optional => Some("NULL".to_string()),
2244 None => None,
2246 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
2248 Some(v) => {
2249 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
2252 Some("options_handle".to_string())
2253 } else {
2254 Some(json_to_c(v))
2255 }
2256 }
2257 }
2258 })
2259 .collect();
2260
2261 parts.join(", ")
2262}
2263
2264fn render_assertion(
2265 out: &mut String,
2266 assertion: &Assertion,
2267 result_var: &str,
2268 ffi_prefix: &str,
2269 _field_resolver: &FieldResolver,
2270 accessed_fields: &[(String, String, bool)],
2271 primitive_locals: &HashMap<String, String>,
2272) {
2273 if let Some(f) = &assertion.field {
2275 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
2276 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
2277 return;
2278 }
2279 }
2280
2281 let field_expr = match &assertion.field {
2282 Some(f) if !f.is_empty() => {
2283 accessed_fields
2285 .iter()
2286 .find(|(k, _, _)| k == f)
2287 .map(|(_, local, _)| local.clone())
2288 .unwrap_or_else(|| result_var.to_string())
2289 }
2290 _ => result_var.to_string(),
2291 };
2292
2293 let field_is_primitive = primitive_locals.contains_key(&field_expr);
2294 let field_primitive_type = primitive_locals.get(&field_expr).cloned();
2295 let field_is_map_access = if let Some(f) = &assertion.field {
2299 accessed_fields.iter().any(|(k, _, m)| k == f && *m)
2300 } else {
2301 false
2302 };
2303
2304 let assertion_field_is_optional = assertion
2308 .field
2309 .as_deref()
2310 .map(|f| {
2311 if f.is_empty() {
2312 return false;
2313 }
2314 if _field_resolver.is_optional(f) {
2315 return true;
2316 }
2317 let resolved = _field_resolver.resolve(f);
2319 _field_resolver.is_optional(resolved)
2320 })
2321 .unwrap_or(false);
2322
2323 match assertion.assertion_type.as_str() {
2324 "equals" => {
2325 if let Some(expected) = &assertion.value {
2326 let c_val = json_to_c(expected);
2327 if field_is_primitive {
2328 let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
2329 match expected.as_bool() {
2330 Some(true) => "1".to_string(),
2331 Some(false) => "0".to_string(),
2332 None => c_val,
2333 }
2334 } else {
2335 c_val
2336 };
2337 let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
2340 if assertion_field_is_optional && is_numeric {
2341 let _ = writeln!(
2342 out,
2343 " assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
2344 );
2345 } else {
2346 let _ = writeln!(
2347 out,
2348 " assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
2349 );
2350 }
2351 } else if expected.is_string() {
2352 let _ = writeln!(
2353 out,
2354 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
2355 );
2356 } else if field_is_map_access && expected.is_boolean() {
2357 let lit = match expected.as_bool() {
2358 Some(true) => "\"true\"",
2359 _ => "\"false\"",
2360 };
2361 let _ = writeln!(
2362 out,
2363 " assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
2364 );
2365 } else if field_is_map_access && expected.is_number() {
2366 if expected.is_f64() {
2367 let _ = writeln!(
2368 out,
2369 " assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
2370 );
2371 } else {
2372 let _ = writeln!(
2373 out,
2374 " assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
2375 );
2376 }
2377 } else {
2378 let _ = writeln!(
2379 out,
2380 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
2381 );
2382 }
2383 }
2384 }
2385 "contains" => {
2386 if let Some(expected) = &assertion.value {
2387 let c_val = json_to_c(expected);
2388 let _ = writeln!(
2389 out,
2390 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
2391 );
2392 }
2393 }
2394 "contains_all" => {
2395 if let Some(values) = &assertion.values {
2396 for val in values {
2397 let c_val = json_to_c(val);
2398 let _ = writeln!(
2399 out,
2400 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
2401 );
2402 }
2403 }
2404 }
2405 "not_contains" => {
2406 if let Some(expected) = &assertion.value {
2407 let c_val = json_to_c(expected);
2408 let _ = writeln!(
2409 out,
2410 " assert(({field_expr} == NULL || strstr({field_expr}, {c_val}) == NULL) && \"expected NOT to contain substring\");"
2411 );
2412 }
2413 }
2414 "not_empty" => {
2415 let _ = writeln!(
2416 out,
2417 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
2418 );
2419 }
2420 "is_empty" => {
2421 if assertion_field_is_optional || !field_is_primitive {
2422 let _ = writeln!(
2424 out,
2425 " assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
2426 );
2427 } else {
2428 let _ = writeln!(
2429 out,
2430 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
2431 );
2432 }
2433 }
2434 "contains_any" => {
2435 if let Some(values) = &assertion.values {
2436 let _ = writeln!(out, " {{");
2437 let _ = writeln!(out, " int found = 0;");
2438 for val in values {
2439 let c_val = json_to_c(val);
2440 let _ = writeln!(
2441 out,
2442 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
2443 );
2444 }
2445 let _ = writeln!(
2446 out,
2447 " assert(found && \"expected to contain at least one of the specified values\");"
2448 );
2449 let _ = writeln!(out, " }}");
2450 }
2451 }
2452 "greater_than" => {
2453 if let Some(val) = &assertion.value {
2454 let c_val = json_to_c(val);
2455 if field_is_map_access && val.is_number() && !field_is_primitive {
2456 let _ = writeln!(
2457 out,
2458 " assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
2459 );
2460 } else {
2461 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
2462 }
2463 }
2464 }
2465 "less_than" => {
2466 if let Some(val) = &assertion.value {
2467 let c_val = json_to_c(val);
2468 if field_is_map_access && val.is_number() && !field_is_primitive {
2469 let _ = writeln!(
2470 out,
2471 " assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
2472 );
2473 } else {
2474 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
2475 }
2476 }
2477 }
2478 "greater_than_or_equal" => {
2479 if let Some(val) = &assertion.value {
2480 let c_val = json_to_c(val);
2481 if field_is_map_access && val.is_number() && !field_is_primitive {
2482 let _ = writeln!(
2483 out,
2484 " assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
2485 );
2486 } else {
2487 let _ = writeln!(
2488 out,
2489 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
2490 );
2491 }
2492 }
2493 }
2494 "less_than_or_equal" => {
2495 if let Some(val) = &assertion.value {
2496 let c_val = json_to_c(val);
2497 if field_is_map_access && val.is_number() && !field_is_primitive {
2498 let _ = writeln!(
2499 out,
2500 " assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
2501 );
2502 } else {
2503 let _ = writeln!(
2504 out,
2505 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
2506 );
2507 }
2508 }
2509 }
2510 "starts_with" => {
2511 if let Some(expected) = &assertion.value {
2512 let c_val = json_to_c(expected);
2513 let _ = writeln!(
2514 out,
2515 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
2516 );
2517 }
2518 }
2519 "ends_with" => {
2520 if let Some(expected) = &assertion.value {
2521 let c_val = json_to_c(expected);
2522 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
2523 let _ = writeln!(
2524 out,
2525 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
2526 );
2527 }
2528 }
2529 "min_length" => {
2530 if let Some(val) = &assertion.value {
2531 if let Some(n) = val.as_u64() {
2532 let _ = writeln!(
2533 out,
2534 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
2535 );
2536 }
2537 }
2538 }
2539 "max_length" => {
2540 if let Some(val) = &assertion.value {
2541 if let Some(n) = val.as_u64() {
2542 let _ = writeln!(
2543 out,
2544 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
2545 );
2546 }
2547 }
2548 }
2549 "count_min" => {
2550 if let Some(val) = &assertion.value {
2551 if let Some(n) = val.as_u64() {
2552 let _ = writeln!(out, " {{");
2553 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
2554 let _ = writeln!(
2555 out,
2556 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
2557 );
2558 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
2559 let _ = writeln!(
2560 out,
2561 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
2562 );
2563 let _ = writeln!(out, " }}");
2564 }
2565 }
2566 }
2567 "count_equals" => {
2568 if let Some(val) = &assertion.value {
2569 if let Some(n) = val.as_u64() {
2570 let _ = writeln!(out, " {{");
2571 let _ = writeln!(out, " /* count_equals: count elements in array */");
2572 let _ = writeln!(
2573 out,
2574 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
2575 );
2576 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
2577 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
2578 let _ = writeln!(out, " }}");
2579 }
2580 }
2581 }
2582 "is_true" => {
2583 let _ = writeln!(out, " assert({field_expr});");
2584 }
2585 "is_false" => {
2586 let _ = writeln!(out, " assert(!{field_expr});");
2587 }
2588 "method_result" => {
2589 if let Some(method_name) = &assertion.method {
2590 render_method_result_assertion(
2591 out,
2592 result_var,
2593 ffi_prefix,
2594 method_name,
2595 assertion.args.as_ref(),
2596 assertion.return_type.as_deref(),
2597 assertion.check.as_deref().unwrap_or("is_true"),
2598 assertion.value.as_ref(),
2599 );
2600 } else {
2601 panic!("C e2e generator: method_result assertion missing 'method' field");
2602 }
2603 }
2604 "matches_regex" => {
2605 if let Some(expected) = &assertion.value {
2606 let c_val = json_to_c(expected);
2607 let _ = writeln!(out, " {{");
2608 let _ = writeln!(out, " regex_t _re;");
2609 let _ = writeln!(
2610 out,
2611 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
2612 );
2613 let _ = writeln!(
2614 out,
2615 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
2616 );
2617 let _ = writeln!(out, " regfree(&_re);");
2618 let _ = writeln!(out, " }}");
2619 }
2620 }
2621 "not_error" => {
2622 }
2624 "error" => {
2625 }
2627 other => {
2628 panic!("C e2e generator: unsupported assertion type: {other}");
2629 }
2630 }
2631}
2632
2633#[allow(clippy::too_many_arguments)]
2642fn render_method_result_assertion(
2643 out: &mut String,
2644 result_var: &str,
2645 ffi_prefix: &str,
2646 method_name: &str,
2647 args: Option<&serde_json::Value>,
2648 return_type: Option<&str>,
2649 check: &str,
2650 value: Option<&serde_json::Value>,
2651) {
2652 let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
2653
2654 if return_type == Some("string") {
2655 let _ = writeln!(out, " {{");
2657 let _ = writeln!(out, " char* _method_result = {call_expr};");
2658 if check == "is_error" {
2659 let _ = writeln!(
2660 out,
2661 " assert(_method_result == NULL && \"expected method to return error\");"
2662 );
2663 let _ = writeln!(out, " }}");
2664 return;
2665 }
2666 let _ = writeln!(
2667 out,
2668 " assert(_method_result != NULL && \"method_result returned NULL\");"
2669 );
2670 match check {
2671 "contains" => {
2672 if let Some(val) = value {
2673 let c_val = json_to_c(val);
2674 let _ = writeln!(
2675 out,
2676 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
2677 );
2678 }
2679 }
2680 "equals" => {
2681 if let Some(val) = value {
2682 let c_val = json_to_c(val);
2683 let _ = writeln!(
2684 out,
2685 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
2686 );
2687 }
2688 }
2689 "is_true" => {
2690 let _ = writeln!(
2691 out,
2692 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
2693 );
2694 }
2695 "count_min" => {
2696 if let Some(val) = value {
2697 let n = val.as_u64().unwrap_or(0);
2698 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
2699 let _ = writeln!(
2700 out,
2701 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
2702 );
2703 }
2704 }
2705 other_check => {
2706 panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
2707 }
2708 }
2709 let _ = writeln!(out, " free(_method_result);");
2710 let _ = writeln!(out, " }}");
2711 return;
2712 }
2713
2714 match check {
2716 "equals" => {
2717 if let Some(val) = value {
2718 let c_val = json_to_c(val);
2719 let _ = writeln!(
2720 out,
2721 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
2722 );
2723 }
2724 }
2725 "is_true" => {
2726 let _ = writeln!(
2727 out,
2728 " assert({call_expr} && \"method_result is_true assertion failed\");"
2729 );
2730 }
2731 "is_false" => {
2732 let _ = writeln!(
2733 out,
2734 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
2735 );
2736 }
2737 "greater_than_or_equal" => {
2738 if let Some(val) = value {
2739 let n = val.as_u64().unwrap_or(0);
2740 let _ = writeln!(
2741 out,
2742 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
2743 );
2744 }
2745 }
2746 "count_min" => {
2747 if let Some(val) = value {
2748 let n = val.as_u64().unwrap_or(0);
2749 let _ = writeln!(
2750 out,
2751 " assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
2752 );
2753 }
2754 }
2755 other_check => {
2756 panic!("C e2e generator: unsupported method_result check type: {other_check}");
2757 }
2758 }
2759}
2760
2761fn build_c_method_call(
2768 result_var: &str,
2769 ffi_prefix: &str,
2770 method_name: &str,
2771 args: Option<&serde_json::Value>,
2772) -> String {
2773 let extra_args = if let Some(args_val) = args {
2774 args_val
2775 .as_object()
2776 .map(|obj| {
2777 obj.values()
2778 .map(|v| match v {
2779 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
2780 serde_json::Value::Bool(true) => "1".to_string(),
2781 serde_json::Value::Bool(false) => "0".to_string(),
2782 serde_json::Value::Number(n) => n.to_string(),
2783 serde_json::Value::Null => "NULL".to_string(),
2784 other => format!("\"{}\"", escape_c(&other.to_string())),
2785 })
2786 .collect::<Vec<_>>()
2787 .join(", ")
2788 })
2789 .unwrap_or_default()
2790 } else {
2791 String::new()
2792 };
2793
2794 if extra_args.is_empty() {
2795 format!("{ffi_prefix}_{method_name}({result_var})")
2796 } else {
2797 format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
2798 }
2799}
2800
2801fn json_to_c(value: &serde_json::Value) -> String {
2803 match value {
2804 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
2805 serde_json::Value::Bool(true) => "1".to_string(),
2806 serde_json::Value::Bool(false) => "0".to_string(),
2807 serde_json::Value::Number(n) => n.to_string(),
2808 serde_json::Value::Null => "NULL".to_string(),
2809 other => format!("\"{}\"", escape_c(&other.to_string())),
2810 }
2811}