1use crate::config::E2eConfig;
7use crate::escape::{escape_c, sanitize_filename, sanitize_ident};
8use crate::field_access::FieldResolver;
9use crate::fixture::{Assertion, Fixture, FixtureGroup};
10use alef_core::backend::GeneratedFile;
11use alef_core::config::AlefConfig;
12use anyhow::Result;
13use std::fmt::Write as FmtWrite;
14use std::path::PathBuf;
15
16use super::E2eCodegen;
17
18pub struct CCodegen;
20
21impl E2eCodegen for CCodegen {
22 fn generate(
23 &self,
24 groups: &[FixtureGroup],
25 e2e_config: &E2eConfig,
26 _alef_config: &AlefConfig,
27 ) -> Result<Vec<GeneratedFile>> {
28 let lang = self.language_name();
29 let output_base = PathBuf::from(&e2e_config.output).join(lang);
30
31 let mut files = Vec::new();
32
33 let call = &e2e_config.call;
35 let overrides = call.overrides.get(lang);
36 let function_name = overrides
37 .and_then(|o| o.function.as_ref())
38 .cloned()
39 .unwrap_or_else(|| call.function.clone());
40 let result_var = &call.result_var;
41 let prefix = overrides.and_then(|o| o.prefix.as_ref()).cloned().unwrap_or_default();
42 let header = overrides
43 .and_then(|o| o.header.as_ref())
44 .cloned()
45 .unwrap_or_else(|| format!("{}.h", call.module));
46
47 let c_pkg = e2e_config.packages.get("c");
49 let include_path = c_pkg
50 .and_then(|p| p.path.as_ref())
51 .cloned()
52 .unwrap_or_else(|| "../../crates/ffi/include".to_string());
53 let lib_path = c_pkg
54 .and_then(|p| p.module.as_ref())
55 .cloned()
56 .unwrap_or_else(|| "../../target/release".to_string());
57 let lib_name = c_pkg
58 .and_then(|p| p.name.as_ref())
59 .cloned()
60 .unwrap_or_else(|| call.module.clone());
61
62 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
64 .iter()
65 .filter_map(|group| {
66 let active: Vec<&Fixture> = group
67 .fixtures
68 .iter()
69 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
70 .collect();
71 if active.is_empty() { None } else { Some((group, active)) }
72 })
73 .collect();
74
75 let category_names: Vec<String> = active_groups
77 .iter()
78 .map(|(g, _)| sanitize_filename(&g.category))
79 .collect();
80 files.push(GeneratedFile {
81 path: output_base.join("Makefile"),
82 content: render_makefile(&category_names, &include_path, &lib_path, &lib_name),
83 generated_header: true,
84 });
85
86 files.push(GeneratedFile {
88 path: output_base.join("test_runner.h"),
89 content: render_test_runner_header(&active_groups),
90 generated_header: true,
91 });
92
93 files.push(GeneratedFile {
95 path: output_base.join("main.c"),
96 content: render_main_c(&active_groups),
97 generated_header: true,
98 });
99
100 let field_resolver = FieldResolver::new(&e2e_config.fields, &e2e_config.fields_optional);
101
102 for (group, active) in &active_groups {
104 let filename = format!("test_{}.c", sanitize_filename(&group.category));
105 let content = render_test_file(
106 &group.category,
107 active,
108 &header,
109 &prefix,
110 &function_name,
111 result_var,
112 &e2e_config.call.args,
113 &field_resolver,
114 );
115 files.push(GeneratedFile {
116 path: output_base.join(filename),
117 content,
118 generated_header: true,
119 });
120 }
121
122 Ok(files)
123 }
124
125 fn language_name(&self) -> &'static str {
126 "c"
127 }
128}
129
130fn render_makefile(categories: &[String], include_path: &str, lib_path: &str, lib_name: &str) -> String {
131 let mut out = String::new();
132 let _ = writeln!(out, "CC = gcc");
133 let _ = writeln!(out, "CFLAGS = -Wall -Wextra -I{include_path}");
134 let _ = writeln!(out, "LDFLAGS = -L{lib_path} -l{lib_name}");
135 let _ = writeln!(out);
136
137 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
138 let srcs = src_files.join(" ");
139
140 let _ = writeln!(out, "SRCS = main.c {srcs}");
141 let _ = writeln!(out, "TARGET = run_tests");
142 let _ = writeln!(out);
143 let _ = writeln!(out, ".PHONY: all clean test");
144 let _ = writeln!(out);
145 let _ = writeln!(out, "all: $(TARGET)");
146 let _ = writeln!(out);
147 let _ = writeln!(out, "$(TARGET): $(SRCS)");
148 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
149 let _ = writeln!(out);
150 let _ = writeln!(out, "test: $(TARGET)");
151 let _ = writeln!(out, "\t./$(TARGET)");
152 let _ = writeln!(out);
153 let _ = writeln!(out, "clean:");
154 let _ = writeln!(out, "\trm -f $(TARGET)");
155 out
156}
157
158fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
159 let mut out = String::new();
160 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
161 let _ = writeln!(out, "#define TEST_RUNNER_H");
162 let _ = writeln!(out);
163
164 for (group, fixtures) in active_groups {
165 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
166 for fixture in fixtures {
167 let fn_name = sanitize_ident(&fixture.id);
168 let _ = writeln!(out, "void test_{fn_name}(void);");
169 }
170 let _ = writeln!(out);
171 }
172
173 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
174 out
175}
176
177fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
178 let mut out = String::new();
179 let _ = writeln!(out, "#include <stdio.h>");
180 let _ = writeln!(out, "#include \"test_runner.h\"");
181 let _ = writeln!(out);
182 let _ = writeln!(out, "int main(void) {{");
183 let _ = writeln!(out, " int passed = 0;");
184 let _ = writeln!(out, " int failed = 0;");
185 let _ = writeln!(out);
186
187 for (group, fixtures) in active_groups {
188 let _ = writeln!(out, " /* Category: {} */", group.category);
189 for fixture in fixtures {
190 let fn_name = sanitize_ident(&fixture.id);
191 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
192 let _ = writeln!(out, " test_{fn_name}();");
193 let _ = writeln!(out, " printf(\" PASSED\\n\");");
194 let _ = writeln!(out, " passed++;");
195 }
196 let _ = writeln!(out);
197 }
198
199 let _ = writeln!(
200 out,
201 " printf(\"\\nResults: %d passed, %d failed\\n\", passed, failed);"
202 );
203 let _ = writeln!(out, " return failed > 0 ? 1 : 0;");
204 let _ = writeln!(out, "}}");
205 out
206}
207
208fn render_test_file(
209 category: &str,
210 fixtures: &[&Fixture],
211 header: &str,
212 prefix: &str,
213 function_name: &str,
214 result_var: &str,
215 args: &[crate::config::ArgMapping],
216 field_resolver: &FieldResolver,
217) -> String {
218 let mut out = String::new();
219 let _ = writeln!(out, "/* E2e tests for category: {category} */");
220 let _ = writeln!(out);
221 let _ = writeln!(out, "#include <assert.h>");
222 let _ = writeln!(out, "#include <string.h>");
223 let _ = writeln!(out, "#include <stdio.h>");
224 let _ = writeln!(out, "#include <stdlib.h>");
225 let _ = writeln!(out, "#include \"{header}\"");
226 let _ = writeln!(out);
227
228 for (i, fixture) in fixtures.iter().enumerate() {
229 render_test_function(
230 &mut out,
231 fixture,
232 prefix,
233 function_name,
234 result_var,
235 args,
236 field_resolver,
237 );
238 if i + 1 < fixtures.len() {
239 let _ = writeln!(out);
240 }
241 }
242
243 out
244}
245
246fn render_test_function(
247 out: &mut String,
248 fixture: &Fixture,
249 prefix: &str,
250 function_name: &str,
251 result_var: &str,
252 args: &[crate::config::ArgMapping],
253 field_resolver: &FieldResolver,
254) {
255 let fn_name = sanitize_ident(&fixture.id);
256 let description = &fixture.description;
257
258 let prefixed_fn = if prefix.is_empty() {
259 function_name.to_string()
260 } else {
261 format!("{prefix}_{function_name}")
262 };
263
264 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
265
266 let args_str = build_args_string(&fixture.input, args);
267
268 let _ = writeln!(out, "void test_{fn_name}(void) {{");
269 let _ = writeln!(out, " /* {description} */");
270
271 if expects_error {
272 let _ = writeln!(out, " const char* {result_var} = {prefixed_fn}({args_str});");
273 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
274 let _ = writeln!(out, "}}");
275 return;
276 }
277
278 let _ = writeln!(out, " const char* {result_var} = {prefixed_fn}({args_str});");
279 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
280
281 for assertion in &fixture.assertions {
282 render_assertion(out, assertion, result_var, field_resolver);
283 }
284
285 let _ = writeln!(out, "}}");
286}
287
288fn build_args_string(input: &serde_json::Value, args: &[crate::config::ArgMapping]) -> String {
289 if args.is_empty() {
290 return json_to_c(input);
291 }
292
293 let parts: Vec<String> = args
294 .iter()
295 .filter_map(|arg| {
296 let val = input.get(&arg.field)?;
297 if val.is_null() && arg.optional {
298 return None;
299 }
300 Some(json_to_c(val))
301 })
302 .collect();
303
304 parts.join(", ")
305}
306
307fn render_assertion(out: &mut String, assertion: &Assertion, result_var: &str, field_resolver: &FieldResolver) {
308 let field_expr = match &assertion.field {
309 Some(f) if !f.is_empty() => field_resolver.accessor(f, "c", result_var),
310 _ => result_var.to_string(),
311 };
312
313 match assertion.assertion_type.as_str() {
314 "equals" => {
315 if let Some(expected) = &assertion.value {
316 let c_val = json_to_c(expected);
317 let _ = writeln!(
318 out,
319 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
320 );
321 }
322 }
323 "contains" => {
324 if let Some(expected) = &assertion.value {
325 let c_val = json_to_c(expected);
326 let _ = writeln!(
327 out,
328 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
329 );
330 }
331 }
332 "contains_all" => {
333 if let Some(values) = &assertion.values {
334 for val in values {
335 let c_val = json_to_c(val);
336 let _ = writeln!(
337 out,
338 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
339 );
340 }
341 }
342 }
343 "not_contains" => {
344 if let Some(expected) = &assertion.value {
345 let c_val = json_to_c(expected);
346 let _ = writeln!(
347 out,
348 " assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
349 );
350 }
351 }
352 "not_empty" => {
353 let _ = writeln!(
354 out,
355 " assert(strlen({field_expr}) > 0 && \"expected non-empty value\");"
356 );
357 }
358 "is_empty" => {
359 let _ = writeln!(
360 out,
361 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
362 );
363 }
364 "starts_with" => {
365 if let Some(expected) = &assertion.value {
366 let c_val = json_to_c(expected);
367 let _ = writeln!(
368 out,
369 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
370 );
371 }
372 }
373 "ends_with" => {
374 if let Some(expected) = &assertion.value {
375 let c_val = json_to_c(expected);
376 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
377 let _ = writeln!(
378 out,
379 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
380 );
381 }
382 }
383 "min_length" => {
384 if let Some(val) = &assertion.value {
385 if let Some(n) = val.as_u64() {
386 let _ = writeln!(
387 out,
388 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
389 );
390 }
391 }
392 }
393 "max_length" => {
394 if let Some(val) = &assertion.value {
395 if let Some(n) = val.as_u64() {
396 let _ = writeln!(
397 out,
398 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
399 );
400 }
401 }
402 }
403 "not_error" => {
404 }
406 "error" => {
407 }
409 other => {
410 let _ = writeln!(out, " /* TODO: unsupported assertion type: {other} */");
411 }
412 }
413}
414
415fn json_to_c(value: &serde_json::Value) -> String {
417 match value {
418 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
419 serde_json::Value::Bool(true) => "1".to_string(),
420 serde_json::Value::Bool(false) => "0".to_string(),
421 serde_json::Value::Number(n) => n.to_string(),
422 serde_json::Value::Null => "NULL".to_string(),
423 other => format!("\"{}\"", escape_c(&other.to_string())),
424 }
425}