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 )
48}
49
50impl E2eCodegen for CCodegen {
51 fn generate(
52 &self,
53 groups: &[FixtureGroup],
54 e2e_config: &E2eConfig,
55 config: &ResolvedCrateConfig,
56 ) -> Result<Vec<GeneratedFile>> {
57 let lang = self.language_name();
58 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
59
60 let mut files = Vec::new();
61
62 let call = &e2e_config.call;
64 let overrides = call.overrides.get(lang);
65 let result_var = &call.result_var;
66 let prefix = overrides
67 .and_then(|o| o.prefix.as_ref())
68 .cloned()
69 .or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
70 .unwrap_or_default();
71 let header = overrides
72 .and_then(|o| o.header.as_ref())
73 .cloned()
74 .unwrap_or_else(|| config.ffi_header_name());
75
76 let c_pkg = e2e_config.resolve_package("c");
78 let lib_name = c_pkg
79 .as_ref()
80 .and_then(|p| p.name.as_ref())
81 .cloned()
82 .unwrap_or_else(|| config.ffi_lib_name());
83
84 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
86 .iter()
87 .filter_map(|group| {
88 let active: Vec<&Fixture> = group
89 .fixtures
90 .iter()
91 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
92 .filter(|f| f.visitor.is_none())
93 .collect();
94 if active.is_empty() { None } else { Some((group, active)) }
95 })
96 .collect();
97
98 let ffi_crate_path = c_pkg
106 .as_ref()
107 .and_then(|p| p.path.as_ref())
108 .cloned()
109 .unwrap_or_else(|| config.ffi_crate_path());
110
111 let category_names: Vec<String> = active_groups
113 .iter()
114 .map(|(g, _)| sanitize_filename(&g.category))
115 .collect();
116 files.push(GeneratedFile {
117 path: output_base.join("Makefile"),
118 content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name),
119 generated_header: true,
120 });
121
122 let github_repo = config.github_repo();
124 let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
125 let ffi_pkg_name = e2e_config
126 .registry
127 .packages
128 .get("c")
129 .and_then(|p| p.name.as_ref())
130 .cloned()
131 .unwrap_or_else(|| lib_name.clone());
132 files.push(GeneratedFile {
133 path: output_base.join("download_ffi.sh"),
134 content: render_download_script(&github_repo, &version, &ffi_pkg_name),
135 generated_header: true,
136 });
137
138 files.push(GeneratedFile {
140 path: output_base.join("test_runner.h"),
141 content: render_test_runner_header(&active_groups),
142 generated_header: true,
143 });
144
145 files.push(GeneratedFile {
147 path: output_base.join("main.c"),
148 content: render_main_c(&active_groups),
149 generated_header: true,
150 });
151
152 let field_resolver = FieldResolver::new(
153 &e2e_config.fields,
154 &e2e_config.fields_optional,
155 &e2e_config.result_fields,
156 &e2e_config.fields_array,
157 &std::collections::HashSet::new(),
158 );
159
160 for (group, active) in &active_groups {
164 let filename = format!("test_{}.c", sanitize_filename(&group.category));
165 let content = render_test_file(
166 &group.category,
167 active,
168 &header,
169 &prefix,
170 result_var,
171 e2e_config,
172 lang,
173 &field_resolver,
174 );
175 files.push(GeneratedFile {
176 path: output_base.join(filename),
177 content,
178 generated_header: true,
179 });
180 }
181
182 Ok(files)
183 }
184
185 fn language_name(&self) -> &'static str {
186 "c"
187 }
188}
189
190struct ResolvedCallInfo {
192 function_name: String,
193 result_type_name: String,
194 options_type_name: String,
195 client_factory: Option<String>,
196 args: Vec<crate::config::ArgMapping>,
197 raw_c_result_type: Option<String>,
198 c_free_fn: Option<String>,
199 result_is_option: bool,
200 result_is_bytes: bool,
206 extra_args: Vec<String>,
210}
211
212fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
213 let overrides = call.overrides.get(lang);
214 let function_name = overrides
215 .and_then(|o| o.function.as_ref())
216 .cloned()
217 .unwrap_or_else(|| call.function.clone());
218 let result_type_name = overrides
223 .and_then(|o| o.result_type.as_ref())
224 .cloned()
225 .unwrap_or_else(|| call.function.to_pascal_case());
226 let options_type_name = overrides
227 .and_then(|o| o.options_type.as_deref())
228 .unwrap_or("ConversionOptions")
229 .to_string();
230 let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
231 let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
232 let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
233 let result_is_option = overrides
234 .and_then(|o| if o.result_is_option { Some(true) } else { None })
235 .unwrap_or(call.result_is_option);
236 let result_is_bytes = call.result_is_bytes || overrides.is_some_and(|o| o.result_is_bytes);
241 let extra_args = overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
242 ResolvedCallInfo {
243 function_name,
244 result_type_name,
245 options_type_name,
246 client_factory,
247 args: call.args.clone(),
248 raw_c_result_type,
249 c_free_fn,
250 result_is_option,
251 result_is_bytes,
252 extra_args,
253 }
254}
255
256fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
262 let call = e2e_config.resolve_call(fixture.call.as_deref());
263 let mut info = resolve_call_info(call, lang);
264
265 if info.client_factory.is_none() {
268 let default_overrides = e2e_config.call.overrides.get(lang);
269 if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
270 info.client_factory = Some(factory.clone());
271 }
272 }
273
274 info
275}
276
277fn render_makefile(categories: &[String], header_name: &str, ffi_crate_path: &str, lib_name: &str) -> String {
278 let mut out = String::new();
279 out.push_str(&hash::header(CommentStyle::Hash));
280 let _ = writeln!(out, "CC = gcc");
281 let _ = writeln!(out, "FFI_DIR = ffi");
282 let _ = writeln!(out);
283
284 let link_lib_name = lib_name.replace('-', "_");
289
290 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
292 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
293 let _ = writeln!(
294 out,
295 " LDFLAGS = -L$(FFI_DIR)/lib -l{link_lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
296 );
297 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
298 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
299 let _ = writeln!(
300 out,
301 " LDFLAGS = -L../../target/release -l{link_lib_name} -Wl,-rpath,../../target/release"
302 );
303 let _ = writeln!(out, "else");
304 let _ = writeln!(
305 out,
306 " CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
307 );
308 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
309 let _ = writeln!(out, "endif");
310 let _ = writeln!(out);
311
312 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
313 let srcs = src_files.join(" ");
314
315 let _ = writeln!(out, "SRCS = main.c {srcs}");
316 let _ = writeln!(out, "TARGET = run_tests");
317 let _ = writeln!(out);
318 let _ = writeln!(out, ".PHONY: all clean test");
319 let _ = writeln!(out);
320 let _ = writeln!(out, "all: $(TARGET)");
321 let _ = writeln!(out);
322 let _ = writeln!(out, "$(TARGET): $(SRCS)");
323 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
324 let _ = writeln!(out);
325 let _ = writeln!(out, "MOCK_SERVER_BIN ?= ../rust/target/release/mock-server");
330 let _ = writeln!(out, "FIXTURES_DIR ?= ../../fixtures");
331 let _ = writeln!(out);
332 let _ = writeln!(out, "test: $(TARGET)");
333 let _ = writeln!(out, "\t@if [ -n \"$$MOCK_SERVER_URL\" ]; then \\");
334 let _ = writeln!(out, "\t\t./$(TARGET); \\");
335 let _ = writeln!(out, "\telse \\");
336 let _ = writeln!(out, "\t\tif [ ! -x \"$(MOCK_SERVER_BIN)\" ]; then \\");
337 let _ = writeln!(
338 out,
339 "\t\t\techo \"mock-server binary not found at $(MOCK_SERVER_BIN); run: cargo build --manifest-path ../rust/Cargo.toml --bin mock-server --release\" >&2; \\"
340 );
341 let _ = writeln!(out, "\t\t\texit 1; \\");
342 let _ = writeln!(out, "\t\tfi; \\");
343 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
344 let _ = writeln!(out, "\t\tmkfifo mock_server.stdin; \\");
345 let _ = writeln!(
346 out,
347 "\t\t\"$(MOCK_SERVER_BIN)\" \"$(FIXTURES_DIR)\" <mock_server.stdin >mock_server.stdout 2>&1 & \\"
348 );
349 let _ = writeln!(out, "\t\tMOCK_PID=$$!; \\");
350 let _ = writeln!(out, "\t\texec 9>mock_server.stdin; \\");
351 let _ = writeln!(out, "\t\tMOCK_URL=\"\"; \\");
352 let _ = writeln!(out, "\t\tfor _ in $$(seq 1 50); do \\");
353 let _ = writeln!(out, "\t\t\tif [ -s mock_server.stdout ]; then \\");
354 let _ = writeln!(
355 out,
356 "\t\t\t\tMOCK_URL=$$(grep -o 'MOCK_SERVER_URL=[^ ]*' mock_server.stdout | head -1 | cut -d= -f2); \\"
357 );
358 let _ = writeln!(out, "\t\t\t\tif [ -n \"$$MOCK_URL\" ]; then break; fi; \\");
359 let _ = writeln!(out, "\t\t\tfi; \\");
360 let _ = writeln!(out, "\t\t\tsleep 0.1; \\");
361 let _ = writeln!(out, "\t\tdone; \\");
362 let _ = writeln!(
363 out,
364 "\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; \\"
365 );
366 let _ = writeln!(out, "\t\tMOCK_SERVER_URL=\"$$MOCK_URL\" ./$(TARGET); STATUS=$$?; \\");
367 let _ = writeln!(out, "\t\texec 9>&-; \\");
368 let _ = writeln!(out, "\t\tkill $$MOCK_PID 2>/dev/null || true; \\");
369 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
370 let _ = writeln!(out, "\t\texit $$STATUS; \\");
371 let _ = writeln!(out, "\tfi");
372 let _ = writeln!(out);
373 let _ = writeln!(out, "clean:");
374 let _ = writeln!(out, "\trm -f $(TARGET) mock_server.stdout mock_server.stdin");
375 out
376}
377
378fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
379 let mut out = String::new();
380 let _ = writeln!(out, "#!/usr/bin/env bash");
381 out.push_str(&hash::header(CommentStyle::Hash));
382 let _ = writeln!(out, "set -euo pipefail");
383 let _ = writeln!(out);
384 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
385 let _ = writeln!(out, "VERSION=\"{version}\"");
386 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
387 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
388 let _ = writeln!(out);
389 let _ = writeln!(out, "# Detect OS and architecture.");
390 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
391 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
392 let _ = writeln!(out);
393 let _ = writeln!(out, "case \"$ARCH\" in");
394 let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
395 let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
396 let _ = writeln!(out, "*)");
397 let _ = writeln!(out, " echo \"Unsupported architecture: $ARCH\" >&2");
398 let _ = writeln!(out, " exit 1");
399 let _ = writeln!(out, " ;;");
400 let _ = writeln!(out, "esac");
401 let _ = writeln!(out);
402 let _ = writeln!(out, "case \"$OS\" in");
403 let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
404 let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
405 let _ = writeln!(out, "*)");
406 let _ = writeln!(out, " echo \"Unsupported OS: $OS\" >&2");
407 let _ = writeln!(out, " exit 1");
408 let _ = writeln!(out, " ;;");
409 let _ = writeln!(out, "esac");
410 let _ = writeln!(out);
411 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
412 let _ = writeln!(
413 out,
414 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
415 );
416 let _ = writeln!(out);
417 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
418 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
419 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
420 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
421 out
422}
423
424fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
425 let mut out = String::new();
426 out.push_str(&hash::header(CommentStyle::Block));
427 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
428 let _ = writeln!(out, "#define TEST_RUNNER_H");
429 let _ = writeln!(out);
430 let _ = writeln!(out, "#include <string.h>");
431 let _ = writeln!(out, "#include <stdlib.h>");
432 let _ = writeln!(out);
433 let _ = writeln!(out, "/**");
435 let _ = writeln!(
436 out,
437 " * Compare a string against an expected value, trimming trailing whitespace."
438 );
439 let _ = writeln!(
440 out,
441 " * Returns 0 if the trimmed actual string equals the expected string."
442 );
443 let _ = writeln!(out, " */");
444 let _ = writeln!(
445 out,
446 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
447 );
448 let _ = writeln!(
449 out,
450 " if (actual == NULL || expected == NULL) return actual != expected;"
451 );
452 let _ = writeln!(out, " size_t alen = strlen(actual);");
453 let _ = writeln!(
454 out,
455 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
456 );
457 let _ = writeln!(out, " size_t elen = strlen(expected);");
458 let _ = writeln!(out, " if (alen != elen) return 1;");
459 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
460 let _ = writeln!(out, "}}");
461 let _ = writeln!(out);
462
463 let _ = writeln!(out, "/**");
464 let _ = writeln!(
465 out,
466 " * Extract a string value for a given key from a JSON object string."
467 );
468 let _ = writeln!(
469 out,
470 " * Returns a heap-allocated copy of the value, or NULL if not found."
471 );
472 let _ = writeln!(out, " * Caller must free() the returned string.");
473 let _ = writeln!(out, " */");
474 let _ = writeln!(
475 out,
476 "static inline char *alef_json_get_string(const char *json, const char *key) {{"
477 );
478 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
479 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
480 let _ = writeln!(out, " size_t key_len = strlen(key);");
481 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
482 let _ = writeln!(out, " if (!pattern) return NULL;");
483 let _ = writeln!(out, " pattern[0] = '\"';");
484 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
485 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
486 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
487 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
488 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
489 let _ = writeln!(out, " free(pattern);");
490 let _ = writeln!(out, " if (!found) return NULL;");
491 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
492 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
493 let _ = writeln!(out, " if (*found != '\"') return NULL; /* not a string value */");
494 let _ = writeln!(out, " found++; /* skip opening quote */");
495 let _ = writeln!(out, " const char *end = found;");
496 let _ = writeln!(out, " while (*end && *end != '\"') {{");
497 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
498 let _ = writeln!(out, " else end++;");
499 let _ = writeln!(out, " }}");
500 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
501 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
502 let _ = writeln!(out, " if (!result_str) return NULL;");
503 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
504 let _ = writeln!(out, " result_str[val_len] = '\\0';");
505 let _ = writeln!(out, " return result_str;");
506 let _ = writeln!(out, "}}");
507 let _ = writeln!(out);
508 let _ = writeln!(out, "/**");
509 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
510 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
511 let _ = writeln!(out, " */");
512 let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
513 let _ = writeln!(out, " if (json == NULL) return 0;");
514 let _ = writeln!(out, " /* Skip leading whitespace */");
515 let _ = writeln!(
516 out,
517 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
518 );
519 let _ = writeln!(out, " if (*json != '[') return 0;");
520 let _ = writeln!(out, " json++;");
521 let _ = writeln!(out, " /* Skip whitespace after '[' */");
522 let _ = writeln!(
523 out,
524 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
525 );
526 let _ = writeln!(out, " if (*json == ']') return 0;");
527 let _ = writeln!(out, " int count = 1;");
528 let _ = writeln!(out, " int depth = 0;");
529 let _ = writeln!(out, " int in_string = 0;");
530 let _ = writeln!(
531 out,
532 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
533 );
534 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
535 let _ = writeln!(
536 out,
537 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
538 );
539 let _ = writeln!(out, " if (in_string) continue;");
540 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
541 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
542 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
543 let _ = writeln!(out, " }}");
544 let _ = writeln!(out, " return count;");
545 let _ = writeln!(out, "}}");
546 let _ = writeln!(out);
547
548 for (group, fixtures) in active_groups {
549 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
550 for fixture in fixtures {
551 let fn_name = sanitize_ident(&fixture.id);
552 let _ = writeln!(out, "void test_{fn_name}(void);");
553 }
554 let _ = writeln!(out);
555 }
556
557 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
558 out
559}
560
561fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
562 let mut out = String::new();
563 out.push_str(&hash::header(CommentStyle::Block));
564 let _ = writeln!(out, "#include <stdio.h>");
565 let _ = writeln!(out, "#include \"test_runner.h\"");
566 let _ = writeln!(out);
567 let _ = writeln!(out, "int main(void) {{");
568 let _ = writeln!(out, " int passed = 0;");
569 let _ = writeln!(out);
570
571 for (group, fixtures) in active_groups {
572 let _ = writeln!(out, " /* Category: {} */", group.category);
573 for fixture in fixtures {
574 let fn_name = sanitize_ident(&fixture.id);
575 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
576 let _ = writeln!(out, " test_{fn_name}();");
577 let _ = writeln!(out, " printf(\" PASSED\\n\");");
578 let _ = writeln!(out, " passed++;");
579 }
580 let _ = writeln!(out);
581 }
582
583 let _ = writeln!(out, " printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
584 let _ = writeln!(out, " return 0;");
585 let _ = writeln!(out, "}}");
586 out
587}
588
589#[allow(clippy::too_many_arguments)]
590fn render_test_file(
591 category: &str,
592 fixtures: &[&Fixture],
593 header: &str,
594 prefix: &str,
595 result_var: &str,
596 e2e_config: &E2eConfig,
597 lang: &str,
598 field_resolver: &FieldResolver,
599) -> String {
600 let mut out = String::new();
601 out.push_str(&hash::header(CommentStyle::Block));
602 let _ = writeln!(out, "/* E2e tests for category: {category} */");
603 let _ = writeln!(out);
604 let _ = writeln!(out, "#include <assert.h>");
605 let _ = writeln!(out, "#include <string.h>");
606 let _ = writeln!(out, "#include <stdio.h>");
607 let _ = writeln!(out, "#include <stdlib.h>");
608 let _ = writeln!(out, "#include \"{header}\"");
609 let _ = writeln!(out, "#include \"test_runner.h\"");
610 let _ = writeln!(out);
611
612 for (i, fixture) in fixtures.iter().enumerate() {
613 if fixture.visitor.is_some() {
616 panic!(
617 "C e2e generator: visitor pattern not supported for fixture: {}",
618 fixture.id
619 );
620 }
621
622 let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
623 render_test_function(
624 &mut out,
625 fixture,
626 prefix,
627 &call_info.function_name,
628 result_var,
629 &call_info.args,
630 field_resolver,
631 &e2e_config.fields_c_types,
632 &call_info.result_type_name,
633 &call_info.options_type_name,
634 call_info.client_factory.as_deref(),
635 call_info.raw_c_result_type.as_deref(),
636 call_info.c_free_fn.as_deref(),
637 call_info.result_is_option,
638 call_info.result_is_bytes,
639 &call_info.extra_args,
640 );
641 if i + 1 < fixtures.len() {
642 let _ = writeln!(out);
643 }
644 }
645
646 out
647}
648
649#[allow(clippy::too_many_arguments)]
650fn render_test_function(
651 out: &mut String,
652 fixture: &Fixture,
653 prefix: &str,
654 function_name: &str,
655 result_var: &str,
656 args: &[crate::config::ArgMapping],
657 field_resolver: &FieldResolver,
658 fields_c_types: &HashMap<String, String>,
659 result_type_name: &str,
660 options_type_name: &str,
661 client_factory: Option<&str>,
662 raw_c_result_type: Option<&str>,
663 c_free_fn: Option<&str>,
664 result_is_option: bool,
665 result_is_bytes: bool,
666 extra_args: &[String],
667) {
668 let fn_name = sanitize_ident(&fixture.id);
669 let description = &fixture.description;
670
671 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
672
673 let _ = writeln!(out, "void test_{fn_name}(void) {{");
674 let _ = writeln!(out, " /* {description} */");
675
676 let prefix_upper = prefix.to_uppercase();
677
678 if client_factory.is_some() && function_name == "chat_stream" {
684 render_chat_stream_test_function(out, fixture, prefix, result_var, args, options_type_name, expects_error);
685 return;
686 }
687
688 if let Some(factory) = client_factory {
696 if result_is_bytes {
697 render_bytes_test_function(
698 out,
699 fixture,
700 prefix,
701 function_name,
702 result_var,
703 args,
704 options_type_name,
705 result_type_name,
706 factory,
707 expects_error,
708 );
709 return;
710 }
711 }
712
713 if let Some(factory) = client_factory {
718 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); let mut inline_method_args: Vec<String> = Vec::new();
723
724 for arg in args {
725 if arg.arg_type == "json_object" {
726 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
731 options_type_name.to_string()
732 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
733 format!("{}Request", stripped)
734 } else {
735 format!("{result_type_name}Request")
736 };
737 let request_type_snake = request_type_pascal.to_snake_case();
738 let var_name = format!("{request_type_snake}_handle");
739
740 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
741 let json_val = if field.is_empty() || field == "input" {
742 Some(&fixture.input)
743 } else {
744 fixture.input.get(field)
745 };
746
747 if let Some(val) = json_val {
748 if !val.is_null() {
749 let normalized = super::normalize_json_keys_to_snake_case(val);
750 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
751 let escaped = escape_c(&json_str);
752 let _ = writeln!(
753 out,
754 " {prefix_upper}{request_type_pascal}* {var_name} = \
755 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
756 );
757 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
758 request_handle_vars.push((arg.name.clone(), var_name));
759 }
760 }
761 } else if arg.arg_type == "string" {
762 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
764 let val = fixture.input.get(field);
765 match val {
766 Some(v) if v.is_string() => {
767 let s = v.as_str().unwrap_or_default();
768 let escaped = escape_c(s);
769 inline_method_args.push(format!("\"{escaped}\""));
770 }
771 Some(serde_json::Value::Null) | None if arg.optional => {
772 inline_method_args.push("NULL".to_string());
773 }
774 None => {
775 inline_method_args.push("\"\"".to_string());
776 }
777 Some(other) => {
778 let s = serde_json::to_string(other).unwrap_or_default();
779 let escaped = escape_c(&s);
780 inline_method_args.push(format!("\"{escaped}\""));
781 }
782 }
783 } else if arg.optional {
784 inline_method_args.push("NULL".to_string());
786 }
787 }
788
789 let fixture_id = &fixture.id;
790 if fixture.needs_mock_server() {
791 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
792 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
793 let _ = writeln!(out, " char base_url[1024];");
794 let _ = writeln!(
795 out,
796 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
797 );
798 let _ = writeln!(
799 out,
800 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, 0, 0, NULL);"
801 );
802 } else {
803 let _ = writeln!(
804 out,
805 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
806 );
807 }
808 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
809
810 let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
811 String::new()
812 } else {
813 let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
814 let parts: Vec<String> = handles
815 .into_iter()
816 .chain(inline_method_args.iter().cloned())
817 .chain(extra_args.iter().cloned())
818 .collect();
819 format!(", {}", parts.join(", "))
820 };
821
822 let call_fn = format!("{prefix}_default_client_{function_name}");
823
824 if expects_error {
825 let _ = writeln!(
826 out,
827 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
828 );
829 for (_, var_name) in &request_handle_vars {
830 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
831 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
832 }
833 let _ = writeln!(out, " {prefix}_default_client_free(client);");
834 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
835 let _ = writeln!(out, "}}");
836 return;
837 }
838
839 let _ = writeln!(
840 out,
841 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
842 );
843 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
844
845 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
846 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
847 let mut primitive_locals: HashMap<String, String> = HashMap::new();
850
851 for assertion in &fixture.assertions {
852 if let Some(f) = &assertion.field {
853 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
854 let resolved = field_resolver.resolve(f);
855 let local_var = f.replace(['.', '['], "_").replace(']', "");
856 let has_map_access = resolved.contains('[');
857 if resolved.contains('.') {
858 let leaf_primitive = emit_nested_accessor(
859 out,
860 prefix,
861 resolved,
862 &local_var,
863 result_var,
864 fields_c_types,
865 &mut intermediate_handles,
866 result_type_name,
867 );
868 if let Some(prim) = leaf_primitive {
869 primitive_locals.insert(local_var.clone(), prim);
870 }
871 } else {
872 let result_type_snake = result_type_name.to_snake_case();
873 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
874 let lookup_key = format!("{result_type_snake}.{resolved}");
875 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
876 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
877 primitive_locals.insert(local_var.clone(), t.clone());
878 } else {
879 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
880 }
881 }
882 accessed_fields.push((f.clone(), local_var, has_map_access));
883 }
884 }
885 }
886
887 for assertion in &fixture.assertions {
888 render_assertion(
889 out,
890 assertion,
891 result_var,
892 prefix,
893 field_resolver,
894 &accessed_fields,
895 &primitive_locals,
896 );
897 }
898
899 for (_f, local_var, from_json) in &accessed_fields {
900 if primitive_locals.contains_key(local_var) {
901 continue;
902 }
903 if *from_json {
904 let _ = writeln!(out, " free({local_var});");
905 } else {
906 let _ = writeln!(out, " {prefix}_free_string({local_var});");
907 }
908 }
909 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
910 if snake_type == "free_string" {
911 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
912 } else {
913 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
914 }
915 }
916 let result_type_snake = result_type_name.to_snake_case();
917 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
918 for (_, var_name) in &request_handle_vars {
919 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
920 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
921 }
922 let _ = writeln!(out, " {prefix}_default_client_free(client);");
923 let _ = writeln!(out, "}}");
924 return;
925 }
926
927 if let Some(raw_type) = raw_c_result_type {
930 let args_str = if args.is_empty() {
932 String::new()
933 } else {
934 let parts: Vec<String> = args
935 .iter()
936 .filter_map(|arg| {
937 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
938 let val = fixture.input.get(field);
939 match val {
940 None if arg.optional => Some("NULL".to_string()),
941 None => None,
942 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
943 Some(v) => Some(json_to_c(v)),
944 }
945 })
946 .collect();
947 parts.join(", ")
948 };
949
950 let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
952
953 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
955 if has_not_error {
956 match raw_type {
957 "char*" if !result_is_option => {
958 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
959 }
960 "int32_t" => {
961 let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
962 }
963 "uintptr_t" => {
964 let _ = writeln!(
965 out,
966 " assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
967 );
968 }
969 _ => {}
970 }
971 }
972
973 for assertion in &fixture.assertions {
975 match assertion.assertion_type.as_str() {
976 "not_error" | "error" => {} "not_empty" => {
978 let _ = writeln!(
979 out,
980 " assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
981 );
982 }
983 "is_empty" => {
984 if result_is_option && raw_type == "char*" {
985 let _ = writeln!(
986 out,
987 " assert({result_var} == NULL && \"expected empty/null value\");"
988 );
989 } else {
990 let _ = writeln!(
991 out,
992 " assert(strlen({result_var}) == 0 && \"expected empty value\");"
993 );
994 }
995 }
996 "count_min" => {
997 if let Some(val) = &assertion.value {
998 if let Some(n) = val.as_u64() {
999 match raw_type {
1000 "char*" => {
1001 let _ = writeln!(out, " {{");
1002 let _ = writeln!(
1003 out,
1004 " assert({result_var} != NULL && \"expected non-null JSON array\");"
1005 );
1006 let _ =
1007 writeln!(out, " int elem_count = alef_json_array_count({result_var});");
1008 let _ = writeln!(
1009 out,
1010 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1011 );
1012 let _ = writeln!(out, " }}");
1013 }
1014 _ => {
1015 let _ = writeln!(
1016 out,
1017 " assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
1018 );
1019 }
1020 }
1021 }
1022 }
1023 }
1024 "greater_than_or_equal" => {
1025 if let Some(val) = &assertion.value {
1026 let c_val = json_to_c(val);
1027 let _ = writeln!(
1028 out,
1029 " assert({result_var} >= {c_val} && \"expected greater than or equal\");"
1030 );
1031 }
1032 }
1033 "contains" => {
1034 if let Some(val) = &assertion.value {
1035 let c_val = json_to_c(val);
1036 let _ = writeln!(
1037 out,
1038 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1039 );
1040 }
1041 }
1042 "contains_all" => {
1043 if let Some(values) = &assertion.values {
1044 for val in values {
1045 let c_val = json_to_c(val);
1046 let _ = writeln!(
1047 out,
1048 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1049 );
1050 }
1051 }
1052 }
1053 "equals" => {
1054 if let Some(val) = &assertion.value {
1055 let c_val = json_to_c(val);
1056 if val.is_string() {
1057 let _ = writeln!(
1058 out,
1059 " assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
1060 );
1061 } else {
1062 let _ = writeln!(
1063 out,
1064 " assert({result_var} == {c_val} && \"equals assertion failed\");"
1065 );
1066 }
1067 }
1068 }
1069 "not_contains" => {
1070 if let Some(val) = &assertion.value {
1071 let c_val = json_to_c(val);
1072 let _ = writeln!(
1073 out,
1074 " assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1075 );
1076 }
1077 }
1078 "starts_with" => {
1079 if let Some(val) = &assertion.value {
1080 let c_val = json_to_c(val);
1081 let _ = writeln!(
1082 out,
1083 " assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1084 );
1085 }
1086 }
1087 "is_true" => {
1088 let _ = writeln!(out, " assert({result_var});");
1089 }
1090 "is_false" => {
1091 let _ = writeln!(out, " assert(!{result_var});");
1092 }
1093 other => {
1094 panic!("C e2e raw-result generator: unsupported assertion type: {other}");
1095 }
1096 }
1097 }
1098
1099 if raw_type == "char*" {
1101 let free_fn = c_free_fn
1102 .map(|s| s.to_string())
1103 .unwrap_or_else(|| format!("{prefix}_free_string"));
1104 if result_is_option {
1105 let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
1106 } else {
1107 let _ = writeln!(out, " {free_fn}({result_var});");
1108 }
1109 }
1110
1111 let _ = writeln!(out, "}}");
1112 return;
1113 }
1114
1115 let prefixed_fn = function_name.to_string();
1121
1122 let mut has_options_handle = false;
1124 for arg in args {
1125 if arg.arg_type == "json_object" {
1126 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1127 if let Some(val) = fixture.input.get(field) {
1128 if !val.is_null() {
1129 let normalized = super::normalize_json_keys_to_snake_case(val);
1133 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1134 let escaped = escape_c(&json_str);
1135 let upper = prefix.to_uppercase();
1136 let options_type_pascal = options_type_name;
1137 let options_type_snake = options_type_name.to_snake_case();
1138 let _ = writeln!(
1139 out,
1140 " {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
1141 );
1142 has_options_handle = true;
1143 }
1144 }
1145 }
1146 }
1147
1148 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
1149
1150 if expects_error {
1151 let _ = writeln!(
1152 out,
1153 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1154 );
1155 if has_options_handle {
1156 let options_type_snake = options_type_name.to_snake_case();
1157 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1158 }
1159 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1160 let _ = writeln!(out, "}}");
1161 return;
1162 }
1163
1164 let _ = writeln!(
1166 out,
1167 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1168 );
1169 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1170
1171 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1179 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1182 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1184
1185 for assertion in &fixture.assertions {
1186 if let Some(f) = &assertion.field {
1187 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1188 let resolved = field_resolver.resolve(f);
1189 let local_var = f.replace(['.', '['], "_").replace(']', "");
1190 let has_map_access = resolved.contains('[');
1191
1192 if resolved.contains('.') {
1193 let leaf_primitive = emit_nested_accessor(
1194 out,
1195 prefix,
1196 resolved,
1197 &local_var,
1198 result_var,
1199 fields_c_types,
1200 &mut intermediate_handles,
1201 result_type_name,
1202 );
1203 if let Some(prim) = leaf_primitive {
1204 primitive_locals.insert(local_var.clone(), prim);
1205 }
1206 } else {
1207 let result_type_snake = result_type_name.to_snake_case();
1208 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1209 let lookup_key = format!("{result_type_snake}.{resolved}");
1210 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1211 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1212 primitive_locals.insert(local_var.clone(), t.clone());
1213 } else {
1214 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1215 }
1216 }
1217 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1218 }
1219 }
1220 }
1221
1222 for assertion in &fixture.assertions {
1223 render_assertion(
1224 out,
1225 assertion,
1226 result_var,
1227 prefix,
1228 field_resolver,
1229 &accessed_fields,
1230 &primitive_locals,
1231 );
1232 }
1233
1234 for (_f, local_var, from_json) in &accessed_fields {
1236 if primitive_locals.contains_key(local_var) {
1237 continue;
1238 }
1239 if *from_json {
1240 let _ = writeln!(out, " free({local_var});");
1241 } else {
1242 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1243 }
1244 }
1245 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1247 if snake_type == "free_string" {
1248 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1250 } else {
1251 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1252 }
1253 }
1254 if has_options_handle {
1255 let options_type_snake = options_type_name.to_snake_case();
1256 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1257 }
1258 let result_type_snake = result_type_name.to_snake_case();
1259 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1260 let _ = writeln!(out, "}}");
1261}
1262
1263#[allow(clippy::too_many_arguments)]
1285fn render_bytes_test_function(
1286 out: &mut String,
1287 fixture: &Fixture,
1288 prefix: &str,
1289 function_name: &str,
1290 _result_var: &str,
1291 args: &[crate::config::ArgMapping],
1292 options_type_name: &str,
1293 result_type_name: &str,
1294 factory: &str,
1295 expects_error: bool,
1296) {
1297 let prefix_upper = prefix.to_uppercase();
1298 let mut request_handle_vars: Vec<(String, String)> = Vec::new();
1299 let mut string_arg_exprs: Vec<String> = Vec::new();
1300
1301 for arg in args {
1302 match arg.arg_type.as_str() {
1303 "json_object" => {
1304 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1305 options_type_name.to_string()
1306 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1307 format!("{}Request", stripped)
1308 } else {
1309 format!("{result_type_name}Request")
1310 };
1311 let request_type_snake = request_type_pascal.to_snake_case();
1312 let var_name = format!("{request_type_snake}_handle");
1313
1314 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1315 let json_val = if field.is_empty() || field == "input" {
1316 Some(&fixture.input)
1317 } else {
1318 fixture.input.get(field)
1319 };
1320
1321 if let Some(val) = json_val {
1322 if !val.is_null() {
1323 let normalized = super::normalize_json_keys_to_snake_case(val);
1324 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1325 let escaped = escape_c(&json_str);
1326 let _ = writeln!(
1327 out,
1328 " {prefix_upper}{request_type_pascal}* {var_name} = \
1329 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1330 );
1331 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1332 request_handle_vars.push((arg.name.clone(), var_name));
1333 }
1334 }
1335 }
1336 "string" => {
1337 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1340 let val = fixture.input.get(field);
1341 let expr = match val {
1342 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
1343 Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
1344 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
1345 None => "NULL".to_string(),
1346 };
1347 string_arg_exprs.push(expr);
1348 }
1349 _ => {
1350 string_arg_exprs.push("NULL".to_string());
1353 }
1354 }
1355 }
1356
1357 let fixture_id = &fixture.id;
1358 if fixture.needs_mock_server() {
1359 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1360 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1361 let _ = writeln!(out, " char base_url[1024];");
1362 let _ = writeln!(
1363 out,
1364 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1365 );
1366 let _ = writeln!(
1367 out,
1368 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, 0, 0, NULL);"
1369 );
1370 } else {
1371 let _ = writeln!(
1372 out,
1373 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
1374 );
1375 }
1376 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1377
1378 let _ = writeln!(out, " uint8_t* out_ptr = NULL;");
1380 let _ = writeln!(out, " uintptr_t out_len = 0;");
1381 let _ = writeln!(out, " uintptr_t out_cap = 0;");
1382
1383 let mut method_args: Vec<String> = Vec::new();
1385 for (_, v) in &request_handle_vars {
1386 method_args.push(v.clone());
1387 }
1388 method_args.extend(string_arg_exprs.iter().cloned());
1389 let extra_args = if method_args.is_empty() {
1390 String::new()
1391 } else {
1392 format!(", {}", method_args.join(", "))
1393 };
1394
1395 let call_fn = format!("{prefix}_default_client_{function_name}");
1396 let _ = writeln!(
1397 out,
1398 " int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
1399 );
1400
1401 if expects_error {
1402 for (_, var_name) in &request_handle_vars {
1403 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1404 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1405 }
1406 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1407 let _ = writeln!(out, " assert(status != 0 && \"expected call to fail\");");
1408 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
1411 let _ = writeln!(out, "}}");
1412 return;
1413 }
1414
1415 let _ = writeln!(out, " assert(status == 0 && \"expected call to succeed\");");
1416
1417 let mut emitted_len_check = false;
1422 for assertion in &fixture.assertions {
1423 match assertion.assertion_type.as_str() {
1424 "not_error" => {
1425 }
1427 "not_empty" | "not_null" => {
1428 if !emitted_len_check {
1429 let _ = writeln!(out, " assert(out_len > 0 && \"expected non-empty value\");");
1430 emitted_len_check = true;
1431 }
1432 }
1433 _ => {
1434 let _ = writeln!(
1438 out,
1439 " /* skipped: assertion '{}' not meaningful on raw byte buffer */",
1440 assertion.assertion_type
1441 );
1442 }
1443 }
1444 }
1445
1446 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
1447 for (_, var_name) in &request_handle_vars {
1448 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1449 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1450 }
1451 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1452 let _ = writeln!(out, "}}");
1453}
1454
1455fn render_chat_stream_test_function(
1466 out: &mut String,
1467 fixture: &Fixture,
1468 prefix: &str,
1469 result_var: &str,
1470 args: &[crate::config::ArgMapping],
1471 options_type_name: &str,
1472 expects_error: bool,
1473) {
1474 let prefix_upper = prefix.to_uppercase();
1475
1476 let mut request_var: Option<String> = None;
1477 for arg in args {
1478 if arg.arg_type == "json_object" {
1479 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1480 options_type_name.to_string()
1481 } else {
1482 "ChatCompletionRequest".to_string()
1483 };
1484 let request_type_snake = request_type_pascal.to_snake_case();
1485 let var_name = format!("{request_type_snake}_handle");
1486
1487 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1488 let json_val = if field.is_empty() || field == "input" {
1489 Some(&fixture.input)
1490 } else {
1491 fixture.input.get(field)
1492 };
1493
1494 if let Some(val) = json_val {
1495 if !val.is_null() {
1496 let normalized = super::normalize_json_keys_to_snake_case(val);
1497 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1498 let escaped = escape_c(&json_str);
1499 let _ = writeln!(
1500 out,
1501 " {prefix_upper}{request_type_pascal}* {var_name} = \
1502 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1503 );
1504 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1505 request_var = Some(var_name);
1506 break;
1507 }
1508 }
1509 }
1510 }
1511
1512 let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
1513 let req_snake = request_var
1514 .as_ref()
1515 .and_then(|v| v.strip_suffix("_handle"))
1516 .unwrap_or("chat_completion_request")
1517 .to_string();
1518
1519 let fixture_id = &fixture.id;
1520 if fixture.needs_mock_server() {
1521 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1522 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1523 let _ = writeln!(out, " char base_url[1024];");
1524 let _ = writeln!(
1525 out,
1526 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1527 );
1528 let _ = writeln!(
1529 out,
1530 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, 0, 0, NULL);"
1531 );
1532 } else {
1533 let _ = writeln!(
1534 out,
1535 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, 0, 0, NULL);"
1536 );
1537 }
1538 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1539
1540 let _ = writeln!(
1541 out,
1542 " {prefix_upper}LiterllmDefaultClientChatStreamStreamHandle* stream_handle = \
1543 {prefix}_default_client_chat_stream_start(client, {req_handle});"
1544 );
1545
1546 if expects_error {
1547 let _ = writeln!(
1548 out,
1549 " assert(stream_handle == NULL && \"expected stream-start to fail\");"
1550 );
1551 if request_var.is_some() {
1552 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
1553 }
1554 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1555 let _ = writeln!(out, "}}");
1556 return;
1557 }
1558
1559 let _ = writeln!(
1560 out,
1561 " assert(stream_handle != NULL && \"expected stream-start to succeed\");"
1562 );
1563
1564 let _ = writeln!(out, " size_t chunks_count = 0;");
1565 let _ = writeln!(out, " char* stream_content = (char*)malloc(1);");
1566 let _ = writeln!(out, " assert(stream_content != NULL);");
1567 let _ = writeln!(out, " stream_content[0] = '\\0';");
1568 let _ = writeln!(out, " size_t stream_content_len = 0;");
1569 let _ = writeln!(out, " int stream_complete = 0;");
1570 let _ = writeln!(out, " int no_chunks_after_done = 1;");
1571 let _ = writeln!(out, " char* last_choices_json = NULL;");
1572 let _ = writeln!(out, " uint64_t total_tokens = 0;");
1573 let _ = writeln!(out);
1574
1575 let _ = writeln!(out, " while (1) {{");
1576 let _ = writeln!(
1577 out,
1578 " {prefix_upper}ChatCompletionChunk* {result_var} = \
1579 {prefix}_default_client_chat_stream_next(stream_handle);"
1580 );
1581 let _ = writeln!(out, " if ({result_var} == NULL) {{");
1582 let _ = writeln!(
1583 out,
1584 " if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
1585 );
1586 let _ = writeln!(out, " break;");
1587 let _ = writeln!(out, " }}");
1588 let _ = writeln!(out, " chunks_count++;");
1589 let _ = writeln!(
1590 out,
1591 " char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
1592 );
1593 let _ = writeln!(out, " if (choices_json != NULL) {{");
1594 let _ = writeln!(
1595 out,
1596 " const char* d = strstr(choices_json, \"\\\"content\\\":\");"
1597 );
1598 let _ = writeln!(out, " if (d != NULL) {{");
1599 let _ = writeln!(out, " d += 10;");
1600 let _ = writeln!(out, " while (*d == ' ' || *d == '\\t') d++;");
1601 let _ = writeln!(out, " if (*d == '\"') {{");
1602 let _ = writeln!(out, " d++;");
1603 let _ = writeln!(out, " const char* e = d;");
1604 let _ = writeln!(out, " while (*e && *e != '\"') {{");
1605 let _ = writeln!(
1606 out,
1607 " if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
1608 );
1609 let _ = writeln!(out, " }}");
1610 let _ = writeln!(out, " size_t add = (size_t)(e - d);");
1611 let _ = writeln!(out, " if (add > 0) {{");
1612 let _ = writeln!(
1613 out,
1614 " char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
1615 );
1616 let _ = writeln!(out, " if (nc != NULL) {{");
1617 let _ = writeln!(out, " stream_content = nc;");
1618 let _ = writeln!(
1619 out,
1620 " memcpy(stream_content + stream_content_len, d, add);"
1621 );
1622 let _ = writeln!(out, " stream_content_len += add;");
1623 let _ = writeln!(
1624 out,
1625 " stream_content[stream_content_len] = '\\0';"
1626 );
1627 let _ = writeln!(out, " }}");
1628 let _ = writeln!(out, " }}");
1629 let _ = writeln!(out, " }}");
1630 let _ = writeln!(out, " }}");
1631 let _ = writeln!(
1632 out,
1633 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
1634 );
1635 let _ = writeln!(out, " last_choices_json = choices_json;");
1636 let _ = writeln!(out, " }}");
1637 let _ = writeln!(
1638 out,
1639 " {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
1640 );
1641 let _ = writeln!(out, " if (usage_handle != NULL) {{");
1642 let _ = writeln!(
1643 out,
1644 " total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
1645 );
1646 let _ = writeln!(out, " {prefix}_usage_free(usage_handle);");
1647 let _ = writeln!(out, " }}");
1648 let _ = writeln!(out, " {prefix}_chat_completion_chunk_free({result_var});");
1649 let _ = writeln!(out, " }}");
1650 let _ = writeln!(out, " {prefix}_default_client_chat_stream_free(stream_handle);");
1651 let _ = writeln!(out);
1652
1653 let _ = writeln!(out, " char* finish_reason = NULL;");
1654 let _ = writeln!(out, " char* tool_calls_json = NULL;");
1655 let _ = writeln!(out, " char* tool_calls_0_function_name = NULL;");
1656 let _ = writeln!(out, " if (last_choices_json != NULL) {{");
1657 let _ = writeln!(
1658 out,
1659 " finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
1660 );
1661 let _ = writeln!(
1662 out,
1663 " const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
1664 );
1665 let _ = writeln!(out, " if (tc != NULL) {{");
1666 let _ = writeln!(out, " tc += 13;");
1667 let _ = writeln!(out, " while (*tc == ' ' || *tc == '\\t') tc++;");
1668 let _ = writeln!(out, " if (*tc == '[') {{");
1669 let _ = writeln!(out, " int depth = 0;");
1670 let _ = writeln!(out, " const char* end = tc;");
1671 let _ = writeln!(out, " int in_str = 0;");
1672 let _ = writeln!(out, " for (; *end; end++) {{");
1673 let _ = writeln!(
1674 out,
1675 " if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
1676 );
1677 let _ = writeln!(
1678 out,
1679 " if (*end == '\"') {{ in_str = !in_str; continue; }}"
1680 );
1681 let _ = writeln!(out, " if (in_str) continue;");
1682 let _ = writeln!(out, " if (*end == '[' || *end == '{{') depth++;");
1683 let _ = writeln!(
1684 out,
1685 " else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
1686 );
1687 let _ = writeln!(out, " }}");
1688 let _ = writeln!(out, " size_t tlen = (size_t)(end - tc);");
1689 let _ = writeln!(out, " tool_calls_json = (char*)malloc(tlen + 1);");
1690 let _ = writeln!(out, " if (tool_calls_json != NULL) {{");
1691 let _ = writeln!(out, " memcpy(tool_calls_json, tc, tlen);");
1692 let _ = writeln!(out, " tool_calls_json[tlen] = '\\0';");
1693 let _ = writeln!(
1694 out,
1695 " const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
1696 );
1697 let _ = writeln!(out, " if (fn != NULL) {{");
1698 let _ = writeln!(
1699 out,
1700 " const char* np = strstr(fn, \"\\\"name\\\":\");"
1701 );
1702 let _ = writeln!(out, " if (np != NULL) {{");
1703 let _ = writeln!(out, " np += 7;");
1704 let _ = writeln!(
1705 out,
1706 " while (*np == ' ' || *np == '\\t') np++;"
1707 );
1708 let _ = writeln!(out, " if (*np == '\"') {{");
1709 let _ = writeln!(out, " np++;");
1710 let _ = writeln!(out, " const char* ne = np;");
1711 let _ = writeln!(
1712 out,
1713 " while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
1714 );
1715 let _ = writeln!(out, " size_t nlen = (size_t)(ne - np);");
1716 let _ = writeln!(
1717 out,
1718 " tool_calls_0_function_name = (char*)malloc(nlen + 1);"
1719 );
1720 let _ = writeln!(
1721 out,
1722 " if (tool_calls_0_function_name != NULL) {{"
1723 );
1724 let _ = writeln!(
1725 out,
1726 " memcpy(tool_calls_0_function_name, np, nlen);"
1727 );
1728 let _ = writeln!(
1729 out,
1730 " tool_calls_0_function_name[nlen] = '\\0';"
1731 );
1732 let _ = writeln!(out, " }}");
1733 let _ = writeln!(out, " }}");
1734 let _ = writeln!(out, " }}");
1735 let _ = writeln!(out, " }}");
1736 let _ = writeln!(out, " }}");
1737 let _ = writeln!(out, " }}");
1738 let _ = writeln!(out, " }}");
1739 let _ = writeln!(out, " }}");
1740 let _ = writeln!(out);
1741
1742 for assertion in &fixture.assertions {
1743 emit_chat_stream_assertion(out, assertion);
1744 }
1745
1746 let _ = writeln!(out, " free(stream_content);");
1747 let _ = writeln!(
1748 out,
1749 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
1750 );
1751 let _ = writeln!(out, " if (finish_reason != NULL) free(finish_reason);");
1752 let _ = writeln!(out, " if (tool_calls_json != NULL) free(tool_calls_json);");
1753 let _ = writeln!(
1754 out,
1755 " if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
1756 );
1757 if request_var.is_some() {
1758 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
1759 }
1760 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1761 let _ = writeln!(
1762 out,
1763 " /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
1764 (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
1765 );
1766 let _ = writeln!(out, "}}");
1767}
1768
1769fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
1773 let field = assertion.field.as_deref().unwrap_or("");
1774
1775 enum Kind {
1776 IntCount,
1777 Bool,
1778 Str,
1779 IntTokens,
1780 Unsupported,
1781 }
1782
1783 let (expr, kind) = match field {
1784 "chunks" => ("chunks_count", Kind::IntCount),
1785 "stream_content" => ("stream_content", Kind::Str),
1786 "stream_complete" => ("stream_complete", Kind::Bool),
1787 "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
1788 "finish_reason" => ("finish_reason", Kind::Str),
1789 "tool_calls" => ("tool_calls_json", Kind::Str),
1790 "tool_calls[0].function.name" => ("tool_calls_0_function_name", Kind::Str),
1791 "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
1792 _ => ("", Kind::Unsupported),
1793 };
1794
1795 let atype = assertion.assertion_type.as_str();
1796 if atype == "not_error" || atype == "error" {
1797 return;
1798 }
1799
1800 if matches!(kind, Kind::Unsupported) {
1801 let _ = writeln!(
1802 out,
1803 " /* skipped: streaming assertion on unsupported field '{field}' */"
1804 );
1805 return;
1806 }
1807
1808 match (atype, &kind) {
1809 ("count_min", Kind::IntCount) => {
1810 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1811 let _ = writeln!(out, " assert({expr} >= {n} && \"expected at least {n} chunks\");");
1812 }
1813 }
1814 ("equals", Kind::Str) => {
1815 if let Some(val) = &assertion.value {
1816 let c_val = json_to_c(val);
1817 let _ = writeln!(
1818 out,
1819 " assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
1820 );
1821 }
1822 }
1823 ("contains", Kind::Str) => {
1824 if let Some(val) = &assertion.value {
1825 let c_val = json_to_c(val);
1826 let _ = writeln!(
1827 out,
1828 " assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
1829 );
1830 }
1831 }
1832 ("not_empty", Kind::Str) => {
1833 let _ = writeln!(
1834 out,
1835 " assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
1836 );
1837 }
1838 ("is_true", Kind::Bool) => {
1839 let _ = writeln!(out, " assert({expr} && \"expected {field} to be true\");");
1840 }
1841 ("is_false", Kind::Bool) => {
1842 let _ = writeln!(out, " assert(!{expr} && \"expected {field} to be false\");");
1843 }
1844 ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
1845 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1846 let _ = writeln!(out, " assert({expr} >= {n} && \"expected {expr} >= {n}\");");
1847 }
1848 }
1849 ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
1850 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1851 let _ = writeln!(out, " assert({expr} == {n} && \"equals assertion failed\");");
1852 }
1853 }
1854 _ => {
1855 let _ = writeln!(
1856 out,
1857 " /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
1858 );
1859 }
1860 }
1861}
1862
1863#[allow(clippy::too_many_arguments)]
1877fn emit_nested_accessor(
1878 out: &mut String,
1879 prefix: &str,
1880 resolved: &str,
1881 local_var: &str,
1882 result_var: &str,
1883 fields_c_types: &HashMap<String, String>,
1884 intermediate_handles: &mut Vec<(String, String)>,
1885 result_type_name: &str,
1886) -> Option<String> {
1887 let segments: Vec<&str> = resolved.split('.').collect();
1888 let prefix_upper = prefix.to_uppercase();
1889
1890 let mut current_snake_type = result_type_name.to_snake_case();
1892 let mut current_handle = result_var.to_string();
1893
1894 for (i, segment) in segments.iter().enumerate() {
1895 let is_leaf = i + 1 == segments.len();
1896
1897 if let Some(bracket_pos) = segment.find('[') {
1899 let field_name = &segment[..bracket_pos];
1900 let key = segment[bracket_pos + 1..].trim_end_matches(']');
1901 let field_snake = field_name.to_snake_case();
1902 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
1903
1904 let json_var = format!("{field_snake}_json");
1907 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
1908 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
1909 let _ = writeln!(out, " assert({json_var} != NULL);");
1910 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
1912 }
1913 let _ = writeln!(
1915 out,
1916 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
1917 );
1918 return None; }
1920
1921 let seg_snake = segment.to_snake_case();
1922 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
1923
1924 if is_leaf {
1925 let lookup_key = format!("{current_snake_type}.{seg_snake}");
1928 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1929 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({current_handle});");
1930 return Some(t.clone());
1931 }
1932 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
1933 } else {
1934 let lookup_key = format!("{current_snake_type}.{seg_snake}");
1936 let return_type_pascal = match fields_c_types.get(&lookup_key) {
1937 Some(t) => t.clone(),
1938 None => {
1939 segment.to_pascal_case()
1941 }
1942 };
1943 let return_snake = return_type_pascal.to_snake_case();
1944 let handle_var = format!("{seg_snake}_handle");
1945
1946 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
1949 let _ = writeln!(
1950 out,
1951 " {prefix_upper}{return_type_pascal}* {handle_var} = \
1952 {accessor_fn}({current_handle});"
1953 );
1954 let _ = writeln!(out, " assert({handle_var} != NULL);");
1955 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
1956 }
1957
1958 current_snake_type = return_snake;
1959 current_handle = handle_var;
1960 }
1961 }
1962 None
1963}
1964
1965fn build_args_string_c(
1969 input: &serde_json::Value,
1970 args: &[crate::config::ArgMapping],
1971 has_options_handle: bool,
1972) -> String {
1973 if args.is_empty() {
1974 return json_to_c(input);
1975 }
1976
1977 let parts: Vec<String> = args
1978 .iter()
1979 .filter_map(|arg| {
1980 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1981 let val = input.get(field);
1982 match val {
1983 None if arg.optional => Some("NULL".to_string()),
1985 None => None,
1987 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1989 Some(v) => {
1990 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
1993 Some("options_handle".to_string())
1994 } else {
1995 Some(json_to_c(v))
1996 }
1997 }
1998 }
1999 })
2000 .collect();
2001
2002 parts.join(", ")
2003}
2004
2005fn render_assertion(
2006 out: &mut String,
2007 assertion: &Assertion,
2008 result_var: &str,
2009 ffi_prefix: &str,
2010 _field_resolver: &FieldResolver,
2011 accessed_fields: &[(String, String, bool)],
2012 primitive_locals: &HashMap<String, String>,
2013) {
2014 if let Some(f) = &assertion.field {
2016 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
2017 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
2018 return;
2019 }
2020 }
2021
2022 let field_expr = match &assertion.field {
2023 Some(f) if !f.is_empty() => {
2024 accessed_fields
2026 .iter()
2027 .find(|(k, _, _)| k == f)
2028 .map(|(_, local, _)| local.clone())
2029 .unwrap_or_else(|| result_var.to_string())
2030 }
2031 _ => result_var.to_string(),
2032 };
2033
2034 let field_is_primitive = primitive_locals.contains_key(&field_expr);
2035 let field_primitive_type = primitive_locals.get(&field_expr).cloned();
2036 let field_is_map_access = if let Some(f) = &assertion.field {
2040 accessed_fields.iter().any(|(k, _, m)| k == f && *m)
2041 } else {
2042 false
2043 };
2044
2045 match assertion.assertion_type.as_str() {
2046 "equals" => {
2047 if let Some(expected) = &assertion.value {
2048 let c_val = json_to_c(expected);
2049 if field_is_primitive {
2050 let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
2051 match expected.as_bool() {
2052 Some(true) => "1".to_string(),
2053 Some(false) => "0".to_string(),
2054 None => c_val,
2055 }
2056 } else {
2057 c_val
2058 };
2059 let _ = writeln!(
2060 out,
2061 " assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
2062 );
2063 } else if expected.is_string() {
2064 let _ = writeln!(
2065 out,
2066 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
2067 );
2068 } else if field_is_map_access && expected.is_boolean() {
2069 let lit = match expected.as_bool() {
2070 Some(true) => "\"true\"",
2071 _ => "\"false\"",
2072 };
2073 let _ = writeln!(
2074 out,
2075 " assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
2076 );
2077 } else if field_is_map_access && expected.is_number() {
2078 if expected.is_f64() {
2079 let _ = writeln!(
2080 out,
2081 " assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
2082 );
2083 } else {
2084 let _ = writeln!(
2085 out,
2086 " assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
2087 );
2088 }
2089 } else {
2090 let _ = writeln!(
2091 out,
2092 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
2093 );
2094 }
2095 }
2096 }
2097 "contains" => {
2098 if let Some(expected) = &assertion.value {
2099 let c_val = json_to_c(expected);
2100 let _ = writeln!(
2101 out,
2102 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
2103 );
2104 }
2105 }
2106 "contains_all" => {
2107 if let Some(values) = &assertion.values {
2108 for val in values {
2109 let c_val = json_to_c(val);
2110 let _ = writeln!(
2111 out,
2112 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
2113 );
2114 }
2115 }
2116 }
2117 "not_contains" => {
2118 if let Some(expected) = &assertion.value {
2119 let c_val = json_to_c(expected);
2120 let _ = writeln!(
2121 out,
2122 " assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
2123 );
2124 }
2125 }
2126 "not_empty" => {
2127 let _ = writeln!(
2128 out,
2129 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
2130 );
2131 }
2132 "is_empty" => {
2133 let _ = writeln!(
2134 out,
2135 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
2136 );
2137 }
2138 "contains_any" => {
2139 if let Some(values) = &assertion.values {
2140 let _ = writeln!(out, " {{");
2141 let _ = writeln!(out, " int found = 0;");
2142 for val in values {
2143 let c_val = json_to_c(val);
2144 let _ = writeln!(
2145 out,
2146 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
2147 );
2148 }
2149 let _ = writeln!(
2150 out,
2151 " assert(found && \"expected to contain at least one of the specified values\");"
2152 );
2153 let _ = writeln!(out, " }}");
2154 }
2155 }
2156 "greater_than" => {
2157 if let Some(val) = &assertion.value {
2158 let c_val = json_to_c(val);
2159 if field_is_map_access && val.is_number() && !field_is_primitive {
2160 let _ = writeln!(
2161 out,
2162 " assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
2163 );
2164 } else {
2165 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
2166 }
2167 }
2168 }
2169 "less_than" => {
2170 if let Some(val) = &assertion.value {
2171 let c_val = json_to_c(val);
2172 if field_is_map_access && val.is_number() && !field_is_primitive {
2173 let _ = writeln!(
2174 out,
2175 " assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
2176 );
2177 } else {
2178 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
2179 }
2180 }
2181 }
2182 "greater_than_or_equal" => {
2183 if let Some(val) = &assertion.value {
2184 let c_val = json_to_c(val);
2185 if field_is_map_access && val.is_number() && !field_is_primitive {
2186 let _ = writeln!(
2187 out,
2188 " assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
2189 );
2190 } else {
2191 let _ = writeln!(
2192 out,
2193 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
2194 );
2195 }
2196 }
2197 }
2198 "less_than_or_equal" => {
2199 if let Some(val) = &assertion.value {
2200 let c_val = json_to_c(val);
2201 if field_is_map_access && val.is_number() && !field_is_primitive {
2202 let _ = writeln!(
2203 out,
2204 " assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
2205 );
2206 } else {
2207 let _ = writeln!(
2208 out,
2209 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
2210 );
2211 }
2212 }
2213 }
2214 "starts_with" => {
2215 if let Some(expected) = &assertion.value {
2216 let c_val = json_to_c(expected);
2217 let _ = writeln!(
2218 out,
2219 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
2220 );
2221 }
2222 }
2223 "ends_with" => {
2224 if let Some(expected) = &assertion.value {
2225 let c_val = json_to_c(expected);
2226 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
2227 let _ = writeln!(
2228 out,
2229 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
2230 );
2231 }
2232 }
2233 "min_length" => {
2234 if let Some(val) = &assertion.value {
2235 if let Some(n) = val.as_u64() {
2236 let _ = writeln!(
2237 out,
2238 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
2239 );
2240 }
2241 }
2242 }
2243 "max_length" => {
2244 if let Some(val) = &assertion.value {
2245 if let Some(n) = val.as_u64() {
2246 let _ = writeln!(
2247 out,
2248 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
2249 );
2250 }
2251 }
2252 }
2253 "count_min" => {
2254 if let Some(val) = &assertion.value {
2255 if let Some(n) = val.as_u64() {
2256 let _ = writeln!(out, " {{");
2257 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
2258 let _ = writeln!(
2259 out,
2260 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
2261 );
2262 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
2263 let _ = writeln!(
2264 out,
2265 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
2266 );
2267 let _ = writeln!(out, " }}");
2268 }
2269 }
2270 }
2271 "count_equals" => {
2272 if let Some(val) = &assertion.value {
2273 if let Some(n) = val.as_u64() {
2274 let _ = writeln!(out, " {{");
2275 let _ = writeln!(out, " /* count_equals: count elements in array */");
2276 let _ = writeln!(
2277 out,
2278 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
2279 );
2280 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
2281 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
2282 let _ = writeln!(out, " }}");
2283 }
2284 }
2285 }
2286 "is_true" => {
2287 let _ = writeln!(out, " assert({field_expr});");
2288 }
2289 "is_false" => {
2290 let _ = writeln!(out, " assert(!{field_expr});");
2291 }
2292 "method_result" => {
2293 if let Some(method_name) = &assertion.method {
2294 render_method_result_assertion(
2295 out,
2296 result_var,
2297 ffi_prefix,
2298 method_name,
2299 assertion.args.as_ref(),
2300 assertion.return_type.as_deref(),
2301 assertion.check.as_deref().unwrap_or("is_true"),
2302 assertion.value.as_ref(),
2303 );
2304 } else {
2305 panic!("C e2e generator: method_result assertion missing 'method' field");
2306 }
2307 }
2308 "matches_regex" => {
2309 if let Some(expected) = &assertion.value {
2310 let c_val = json_to_c(expected);
2311 let _ = writeln!(out, " {{");
2312 let _ = writeln!(out, " regex_t _re;");
2313 let _ = writeln!(
2314 out,
2315 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
2316 );
2317 let _ = writeln!(
2318 out,
2319 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
2320 );
2321 let _ = writeln!(out, " regfree(&_re);");
2322 let _ = writeln!(out, " }}");
2323 }
2324 }
2325 "not_error" => {
2326 }
2328 "error" => {
2329 }
2331 other => {
2332 panic!("C e2e generator: unsupported assertion type: {other}");
2333 }
2334 }
2335}
2336
2337#[allow(clippy::too_many_arguments)]
2346fn render_method_result_assertion(
2347 out: &mut String,
2348 result_var: &str,
2349 ffi_prefix: &str,
2350 method_name: &str,
2351 args: Option<&serde_json::Value>,
2352 return_type: Option<&str>,
2353 check: &str,
2354 value: Option<&serde_json::Value>,
2355) {
2356 let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
2357
2358 if return_type == Some("string") {
2359 let _ = writeln!(out, " {{");
2361 let _ = writeln!(out, " char* _method_result = {call_expr};");
2362 if check == "is_error" {
2363 let _ = writeln!(
2364 out,
2365 " assert(_method_result == NULL && \"expected method to return error\");"
2366 );
2367 let _ = writeln!(out, " }}");
2368 return;
2369 }
2370 let _ = writeln!(
2371 out,
2372 " assert(_method_result != NULL && \"method_result returned NULL\");"
2373 );
2374 match check {
2375 "contains" => {
2376 if let Some(val) = value {
2377 let c_val = json_to_c(val);
2378 let _ = writeln!(
2379 out,
2380 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
2381 );
2382 }
2383 }
2384 "equals" => {
2385 if let Some(val) = value {
2386 let c_val = json_to_c(val);
2387 let _ = writeln!(
2388 out,
2389 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
2390 );
2391 }
2392 }
2393 "is_true" => {
2394 let _ = writeln!(
2395 out,
2396 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
2397 );
2398 }
2399 "count_min" => {
2400 if let Some(val) = value {
2401 let n = val.as_u64().unwrap_or(0);
2402 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
2403 let _ = writeln!(
2404 out,
2405 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
2406 );
2407 }
2408 }
2409 other_check => {
2410 panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
2411 }
2412 }
2413 let _ = writeln!(out, " free(_method_result);");
2414 let _ = writeln!(out, " }}");
2415 return;
2416 }
2417
2418 match check {
2420 "equals" => {
2421 if let Some(val) = value {
2422 let c_val = json_to_c(val);
2423 let _ = writeln!(
2424 out,
2425 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
2426 );
2427 }
2428 }
2429 "is_true" => {
2430 let _ = writeln!(
2431 out,
2432 " assert({call_expr} && \"method_result is_true assertion failed\");"
2433 );
2434 }
2435 "is_false" => {
2436 let _ = writeln!(
2437 out,
2438 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
2439 );
2440 }
2441 "greater_than_or_equal" => {
2442 if let Some(val) = value {
2443 let n = val.as_u64().unwrap_or(0);
2444 let _ = writeln!(
2445 out,
2446 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
2447 );
2448 }
2449 }
2450 "count_min" => {
2451 if let Some(val) = value {
2452 let n = val.as_u64().unwrap_or(0);
2453 let _ = writeln!(
2454 out,
2455 " assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
2456 );
2457 }
2458 }
2459 other_check => {
2460 panic!("C e2e generator: unsupported method_result check type: {other_check}");
2461 }
2462 }
2463}
2464
2465fn build_c_method_call(
2472 result_var: &str,
2473 ffi_prefix: &str,
2474 method_name: &str,
2475 args: Option<&serde_json::Value>,
2476) -> String {
2477 let extra_args = if let Some(args_val) = args {
2478 args_val
2479 .as_object()
2480 .map(|obj| {
2481 obj.values()
2482 .map(|v| match v {
2483 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
2484 serde_json::Value::Bool(true) => "1".to_string(),
2485 serde_json::Value::Bool(false) => "0".to_string(),
2486 serde_json::Value::Number(n) => n.to_string(),
2487 serde_json::Value::Null => "NULL".to_string(),
2488 other => format!("\"{}\"", escape_c(&other.to_string())),
2489 })
2490 .collect::<Vec<_>>()
2491 .join(", ")
2492 })
2493 .unwrap_or_default()
2494 } else {
2495 String::new()
2496 };
2497
2498 if extra_args.is_empty() {
2499 format!("{ffi_prefix}_{method_name}({result_var})")
2500 } else {
2501 format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
2502 }
2503}
2504
2505fn json_to_c(value: &serde_json::Value) -> String {
2507 match value {
2508 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
2509 serde_json::Value::Bool(true) => "1".to_string(),
2510 serde_json::Value::Bool(false) => "0".to_string(),
2511 serde_json::Value::Number(n) => n.to_string(),
2512 serde_json::Value::Null => "NULL".to_string(),
2513 other => format!("\"{}\"", escape_c(&other.to_string())),
2514 }
2515}