1use crate::config::E2eConfig;
19use crate::escape::{escape_shell, sanitize_filename, sanitize_ident};
20use crate::field_access::FieldResolver;
21use crate::fixture::{Assertion, Fixture, FixtureGroup};
22use alef_core::backend::GeneratedFile;
23use alef_core::config::ResolvedCrateConfig;
24use alef_core::hash::{self, CommentStyle};
25use anyhow::Result;
26use std::fmt::Write as FmtWrite;
27use std::path::PathBuf;
28
29use super::E2eCodegen;
30
31pub struct BrewCodegen;
33
34impl E2eCodegen for BrewCodegen {
35 fn generate(
36 &self,
37 groups: &[FixtureGroup],
38 e2e_config: &E2eConfig,
39 _config: &ResolvedCrateConfig,
40 _type_defs: &[alef_core::ir::TypeDef],
41 ) -> Result<Vec<GeneratedFile>> {
42 let lang = self.language_name();
43 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
44
45 let call = &e2e_config.call;
47 let overrides = call.overrides.get(lang);
48 let subcommand = overrides
49 .and_then(|o| o.function.as_ref())
50 .cloned()
51 .unwrap_or_else(|| call.function.clone());
52
53 let static_cli_args: Vec<String> = overrides.map(|o| o.cli_args.clone()).unwrap_or_default();
55
56 let cli_flags: std::collections::HashMap<String, String> =
58 overrides.map(|o| o.cli_flags.clone()).unwrap_or_default();
59
60 let binary_name = e2e_config
62 .registry
63 .packages
64 .get(lang)
65 .and_then(|p| p.name.as_ref())
66 .cloned()
67 .or_else(|| e2e_config.packages.get(lang).and_then(|p| p.name.as_ref()).cloned())
68 .unwrap_or_else(|| call.module.clone());
69
70 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
72 .iter()
73 .filter_map(|group| {
74 let active: Vec<&Fixture> = group
75 .fixtures
76 .iter()
77 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
78 .collect();
79 if active.is_empty() { None } else { Some((group, active)) }
80 })
81 .collect();
82
83 let field_resolver = FieldResolver::new(
84 &e2e_config.fields,
85 &e2e_config.fields_optional,
86 &e2e_config.result_fields,
87 &e2e_config.fields_array,
88 &std::collections::HashSet::new(),
89 );
90
91 let mut files = Vec::new();
92
93 let category_names: Vec<String> = active_groups
95 .iter()
96 .map(|(g, _)| sanitize_filename(&g.category))
97 .collect();
98 files.push(GeneratedFile {
99 path: output_base.join("run_tests.sh"),
100 content: render_run_tests(&category_names),
101 generated_header: true,
102 });
103
104 for (group, active) in &active_groups {
106 let safe_category = sanitize_filename(&group.category);
107 let filename = format!("test_{safe_category}.sh");
108 let content = render_category_file(
109 &group.category,
110 active,
111 &binary_name,
112 &subcommand,
113 &static_cli_args,
114 &cli_flags,
115 &e2e_config.call.args,
116 &field_resolver,
117 e2e_config,
118 );
119 files.push(GeneratedFile {
120 path: output_base.join(filename),
121 content,
122 generated_header: true,
123 });
124 }
125
126 Ok(files)
127 }
128
129 fn language_name(&self) -> &'static str {
130 "brew"
131 }
132}
133
134fn render_run_tests(categories: &[String]) -> String {
136 let mut out = String::new();
137 let _ = writeln!(out, "#!/usr/bin/env bash");
138 out.push_str(&hash::header(CommentStyle::Hash));
139 let _ = writeln!(out, "# shellcheck disable=SC1091");
140 let _ = writeln!(out, "set -euo pipefail");
141 let _ = writeln!(out);
142 let _ = writeln!(out, "# MOCK_SERVER_URL must be set to the base URL of the mock server.");
143 let _ = writeln!(out, ": \"${{MOCK_SERVER_URL:?MOCK_SERVER_URL is required}}\"");
144 let _ = writeln!(out);
145 let _ = writeln!(out, "# Verify that jq is available.");
146 let _ = writeln!(out, "if ! command -v jq &>/dev/null; then");
147 let _ = writeln!(out, " echo 'error: jq is required but not found in PATH' >&2");
148 let _ = writeln!(out, " exit 1");
149 let _ = writeln!(out, "fi");
150 let _ = writeln!(out);
151 let _ = writeln!(out, "PASS=0");
152 let _ = writeln!(out, "FAIL=0");
153 let _ = writeln!(out);
154
155 let _ = writeln!(out, "assert_equals() {{");
157 let _ = writeln!(out, " local actual=\"$1\" expected=\"$2\" label=\"$3\"");
158 let _ = writeln!(out, " if [ \"$actual\" != \"$expected\" ]; then");
159 let _ = writeln!(
160 out,
161 " echo \"FAIL [$label]: expected '$expected', got '$actual'\" >&2"
162 );
163 let _ = writeln!(out, " return 1");
164 let _ = writeln!(out, " fi");
165 let _ = writeln!(out, "}}");
166 let _ = writeln!(out);
167 let _ = writeln!(out, "assert_contains() {{");
168 let _ = writeln!(out, " local actual=\"$1\" expected=\"$2\" label=\"$3\"");
169 let _ = writeln!(out, " if [[ \"$actual\" != *\"$expected\"* ]]; then");
170 let _ = writeln!(
171 out,
172 " echo \"FAIL [$label]: expected to contain '$expected'\" >&2"
173 );
174 let _ = writeln!(out, " return 1");
175 let _ = writeln!(out, " fi");
176 let _ = writeln!(out, "}}");
177 let _ = writeln!(out);
178 let _ = writeln!(out, "assert_not_empty() {{");
179 let _ = writeln!(out, " local actual=\"$1\" label=\"$2\"");
180 let _ = writeln!(out, " if [ -z \"$actual\" ]; then");
181 let _ = writeln!(out, " echo \"FAIL [$label]: expected non-empty value\" >&2");
182 let _ = writeln!(out, " return 1");
183 let _ = writeln!(out, " fi");
184 let _ = writeln!(out, "}}");
185 let _ = writeln!(out);
186 let _ = writeln!(out, "assert_count_min() {{");
187 let _ = writeln!(out, " local count=\"$1\" min=\"$2\" label=\"$3\"");
188 let _ = writeln!(out, " if [ \"$count\" -lt \"$min\" ]; then");
189 let _ = writeln!(
190 out,
191 " echo \"FAIL [$label]: expected at least $min elements, got $count\" >&2"
192 );
193 let _ = writeln!(out, " return 1");
194 let _ = writeln!(out, " fi");
195 let _ = writeln!(out, "}}");
196 let _ = writeln!(out);
197 let _ = writeln!(out, "assert_greater_than() {{");
198 let _ = writeln!(out, " local val=\"$1\" threshold=\"$2\" label=\"$3\"");
199 let _ = writeln!(
200 out,
201 " if [ \"$(echo \"$val > $threshold\" | bc -l)\" != \"1\" ]; then"
202 );
203 let _ = writeln!(out, " echo \"FAIL [$label]: expected $val > $threshold\" >&2");
204 let _ = writeln!(out, " return 1");
205 let _ = writeln!(out, " fi");
206 let _ = writeln!(out, "}}");
207 let _ = writeln!(out);
208 let _ = writeln!(out, "assert_greater_than_or_equal() {{");
209 let _ = writeln!(out, " local actual=\"$1\" expected=\"$2\" label=\"$3\"");
210 let _ = writeln!(out, " if [ \"$actual\" -lt \"$expected\" ]; then");
211 let _ = writeln!(out, " echo \"FAIL [$label]: expected $actual >= $expected\" >&2");
212 let _ = writeln!(out, " return 1");
213 let _ = writeln!(out, " fi");
214 let _ = writeln!(out, "}}");
215 let _ = writeln!(out);
216 let _ = writeln!(out, "assert_is_empty() {{");
217 let _ = writeln!(out, " local actual=\"$1\" label=\"$2\"");
218 let _ = writeln!(out, " if [ -n \"$actual\" ]; then");
219 let _ = writeln!(
220 out,
221 " echo \"FAIL [$label]: expected empty value, got '$actual'\" >&2"
222 );
223 let _ = writeln!(out, " return 1");
224 let _ = writeln!(out, " fi");
225 let _ = writeln!(out, "}}");
226 let _ = writeln!(out);
227 let _ = writeln!(out, "assert_less_than() {{");
228 let _ = writeln!(out, " local actual=\"$1\" expected=\"$2\" label=\"$3\"");
229 let _ = writeln!(out, " if [ \"$actual\" -ge \"$expected\" ]; then");
230 let _ = writeln!(out, " echo \"FAIL [$label]: expected $actual < $expected\" >&2");
231 let _ = writeln!(out, " return 1");
232 let _ = writeln!(out, " fi");
233 let _ = writeln!(out, "}}");
234 let _ = writeln!(out);
235 let _ = writeln!(out, "assert_less_than_or_equal() {{");
236 let _ = writeln!(out, " local actual=\"$1\" expected=\"$2\" label=\"$3\"");
237 let _ = writeln!(out, " if [ \"$actual\" -gt \"$expected\" ]; then");
238 let _ = writeln!(out, " echo \"FAIL [$label]: expected $actual <= $expected\" >&2");
239 let _ = writeln!(out, " return 1");
240 let _ = writeln!(out, " fi");
241 let _ = writeln!(out, "}}");
242 let _ = writeln!(out);
243 let _ = writeln!(out, "assert_not_contains() {{");
244 let _ = writeln!(out, " local actual=\"$1\" expected=\"$2\" label=\"$3\"");
245 let _ = writeln!(out, " if [[ \"$actual\" == *\"$expected\"* ]]; then");
246 let _ = writeln!(
247 out,
248 " echo \"FAIL [$label]: expected not to contain '$expected'\" >&2"
249 );
250 let _ = writeln!(out, " return 1");
251 let _ = writeln!(out, " fi");
252 let _ = writeln!(out, "}}");
253 let _ = writeln!(out);
254
255 let script_dir = r#"SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)""#;
257 let _ = writeln!(out, "{script_dir}");
258 let _ = writeln!(out);
259 for category in categories {
260 let _ = writeln!(out, "# shellcheck source=test_{category}.sh");
261 let _ = writeln!(out, "source \"$SCRIPT_DIR/test_{category}.sh\"");
262 }
263 let _ = writeln!(out);
264
265 let _ = writeln!(out, "run_test() {{");
267 let _ = writeln!(out, " local name=\"$1\"");
268 let _ = writeln!(out, " if \"$name\"; then");
269 let _ = writeln!(out, " echo \"PASS: $name\"");
270 let _ = writeln!(out, " PASS=$((PASS + 1))");
271 let _ = writeln!(out, " else");
272 let _ = writeln!(out, " echo \"FAIL: $name\"");
273 let _ = writeln!(out, " FAIL=$((FAIL + 1))");
274 let _ = writeln!(out, " fi");
275 let _ = writeln!(out, "}}");
276 let _ = writeln!(out);
277
278 let _ = writeln!(out, "# Run all generated test functions.");
282 for category in categories {
283 let _ = writeln!(out, "# Category: {category}");
284 let _ = writeln!(out, "run_tests_{category}");
287 }
288 let _ = writeln!(out);
289 let _ = writeln!(out, "echo \"\"");
290 let _ = writeln!(out, "echo \"Results: $PASS passed, $FAIL failed\"");
291 let _ = writeln!(out, "[ \"$FAIL\" -eq 0 ]");
292 out
293}
294
295#[allow(clippy::too_many_arguments)]
297fn render_category_file(
298 category: &str,
299 fixtures: &[&Fixture],
300 binary_name: &str,
301 subcommand: &str,
302 static_cli_args: &[String],
303 cli_flags: &std::collections::HashMap<String, String>,
304 args: &[crate::config::ArgMapping],
305 field_resolver: &FieldResolver,
306 e2e_config: &E2eConfig,
307) -> String {
308 let safe_category = sanitize_filename(category);
309 let mut out = String::new();
310 let _ = writeln!(out, "#!/usr/bin/env bash");
311 out.push_str(&hash::header(CommentStyle::Hash));
312 let _ = writeln!(out, "# E2e tests for category: {category}");
313 let _ = writeln!(out, "set -euo pipefail");
314 let _ = writeln!(out);
315
316 for fixture in fixtures {
317 render_test_function(
318 &mut out,
319 fixture,
320 binary_name,
321 subcommand,
322 static_cli_args,
323 cli_flags,
324 args,
325 field_resolver,
326 e2e_config,
327 );
328 let _ = writeln!(out);
329 }
330
331 let _ = writeln!(out, "run_tests_{safe_category}() {{");
333 for fixture in fixtures {
334 let fn_name = sanitize_ident(&fixture.id);
335 let _ = writeln!(out, " run_test test_{fn_name}");
336 }
337 let _ = writeln!(out, "}}");
338 out
339}
340
341#[allow(clippy::too_many_arguments)]
343fn render_test_function(
344 out: &mut String,
345 fixture: &Fixture,
346 binary_name: &str,
347 subcommand: &str,
348 static_cli_args: &[String],
349 cli_flags: &std::collections::HashMap<String, String>,
350 _args: &[crate::config::ArgMapping],
351 field_resolver: &FieldResolver,
352 e2e_config: &E2eConfig,
353) {
354 let fn_name = sanitize_ident(&fixture.id);
355 let description = &fixture.description;
356
357 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
358
359 let _ = writeln!(out, "test_{fn_name}() {{");
360 let _ = writeln!(out, " # {description}");
361
362 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
364
365 let cmd_parts = build_cli_command(
367 fixture,
368 binary_name,
369 subcommand,
370 static_cli_args,
371 cli_flags,
372 &call_config.args,
373 );
374
375 if expects_error {
376 let cmd = cmd_parts.join(" ");
377 let _ = writeln!(out, " if {cmd} >/dev/null 2>&1; then");
378 let _ = writeln!(
379 out,
380 " echo 'FAIL [error]: expected command to fail but it succeeded' >&2"
381 );
382 let _ = writeln!(out, " return 1");
383 let _ = writeln!(out, " fi");
384 let _ = writeln!(out, "}}");
385 return;
386 }
387
388 let has_active_assertions = fixture.assertions.iter().any(|a| {
390 a.field
391 .as_ref()
392 .is_none_or(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
393 });
394
395 let cmd = cmd_parts.join(" ");
397 if has_active_assertions {
398 let _ = writeln!(out, " local output");
399 let _ = writeln!(out, " output=$({cmd})");
400 } else {
401 let _ = writeln!(out, " {cmd} >/dev/null");
402 }
403 let _ = writeln!(out);
404
405 for assertion in &fixture.assertions {
407 render_assertion(out, assertion, binary_name, field_resolver);
408 }
409
410 let _ = writeln!(out, "}}");
411}
412
413fn build_cli_command(
418 fixture: &Fixture,
419 binary_name: &str,
420 subcommand: &str,
421 static_cli_args: &[String],
422 cli_flags: &std::collections::HashMap<String, String>,
423 args: &[crate::config::ArgMapping],
424) -> Vec<String> {
425 let mut parts: Vec<String> = vec![binary_name.to_string(), subcommand.to_string()];
426
427 for arg in args {
428 match arg.arg_type.as_str() {
429 "mock_url" => {
430 parts.push(format!("\"${{MOCK_SERVER_URL}}/fixtures/{}\"", fixture.id));
432 }
433 "handle" => {
434 }
436 _ => {
437 if let Some(flag) = cli_flags.get(&arg.field) {
439 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
440 if let Some(val) = fixture.input.get(field) {
441 if !val.is_null() {
442 let val_str = json_value_to_shell_arg(val);
443 parts.push(flag.clone());
444 parts.push(val_str);
445 }
446 }
447 }
448 }
449 }
450 }
451
452 for static_arg in static_cli_args {
454 parts.push(static_arg.clone());
455 }
456
457 parts
458}
459
460fn json_value_to_shell_arg(value: &serde_json::Value) -> String {
465 match value {
466 serde_json::Value::String(s) => format!("'{}'", escape_shell(s)),
467 serde_json::Value::Bool(b) => b.to_string(),
468 serde_json::Value::Number(n) => n.to_string(),
469 serde_json::Value::Null => "''".to_string(),
470 other => format!("'{}'", escape_shell(&other.to_string())),
471 }
472}
473
474fn field_to_jq_path(resolved: &str) -> String {
481 if let Some((prefix, suffix)) = resolved.rsplit_once('.') {
484 if suffix == "length" || suffix == "count" || suffix == "size" {
485 return format!(".{prefix} | length");
486 }
487 }
488 if resolved == "length" || resolved == "count" || resolved == "size" {
490 return ". | length".to_string();
491 }
492 format!(".{resolved}")
493}
494
495fn build_brew_method_call(binary_name: &str, method_name: &str, args: Option<&serde_json::Value>) -> String {
502 let subcommand = method_name.replace('_', "-");
503 if let Some(args_val) = args {
504 let arg_str = args_val
505 .as_object()
506 .map(|obj| {
507 obj.values()
508 .map(|v| match v {
509 serde_json::Value::String(s) => format!("'{}'", escape_shell(s)),
510 other => other.to_string(),
511 })
512 .collect::<Vec<_>>()
513 .join(" ")
514 })
515 .unwrap_or_default();
516 if arg_str.is_empty() {
517 format!("{binary_name} {subcommand} \"$output\"")
518 } else {
519 format!("{binary_name} {subcommand} \"$output\" {arg_str}")
520 }
521 } else {
522 format!("{binary_name} {subcommand} \"$output\"")
523 }
524}
525
526fn render_assertion(out: &mut String, assertion: &Assertion, binary_name: &str, field_resolver: &FieldResolver) {
528 if let Some(f) = &assertion.field {
530 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
531 let _ = writeln!(out, " # skipped: field '{f}' not available on result type");
532 return;
533 }
534 }
535
536 match assertion.assertion_type.as_str() {
537 "equals" => {
538 if let Some(field) = &assertion.field {
539 if let Some(expected) = &assertion.value {
540 let resolved = field_resolver.resolve(field);
541 let jq_path = field_to_jq_path(resolved);
542 let expected_str = json_value_to_shell_string(expected);
543 let safe_field = sanitize_ident(field);
544 let _ = writeln!(out, " local val_{safe_field}");
545 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
546 let _ = writeln!(
547 out,
548 " assert_equals \"$val_{safe_field}\" '{expected_str}' '{field}'"
549 );
550 }
551 }
552 }
553 "contains" => {
554 if let Some(field) = &assertion.field {
555 if let Some(expected) = &assertion.value {
556 let resolved = field_resolver.resolve(field);
557 let jq_path = field_to_jq_path(resolved);
558 let expected_str = json_value_to_shell_string(expected);
559 let safe_field = sanitize_ident(field);
560 let _ = writeln!(out, " local val_{safe_field}");
561 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
562 let _ = writeln!(
563 out,
564 " assert_contains \"$val_{safe_field}\" '{expected_str}' '{field}'"
565 );
566 }
567 }
568 }
569 "not_empty" | "tree_not_null" => {
570 if let Some(field) = &assertion.field {
571 let resolved = field_resolver.resolve(field);
572 let jq_path = field_to_jq_path(resolved);
573 let safe_field = sanitize_ident(field);
574 let _ = writeln!(out, " local val_{safe_field}");
575 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
576 let _ = writeln!(out, " assert_not_empty \"$val_{safe_field}\" '{field}'");
577 }
578 }
579 "count_min" | "root_child_count_min" => {
580 if let Some(field) = &assertion.field {
581 if let Some(val) = &assertion.value {
582 if let Some(min) = val.as_u64() {
583 let resolved = field_resolver.resolve(field);
584 let jq_path = field_to_jq_path(resolved);
585 let safe_field = sanitize_ident(field);
586 let _ = writeln!(out, " local count_{safe_field}");
587 let _ = writeln!(
588 out,
589 " count_{safe_field}=$(echo \"$output\" | jq '{jq_path} | length')"
590 );
591 let _ = writeln!(out, " assert_count_min \"$count_{safe_field}\" {min} '{field}'");
592 }
593 }
594 }
595 }
596 "greater_than" => {
597 if let Some(field) = &assertion.field {
598 if let Some(val) = &assertion.value {
599 let resolved = field_resolver.resolve(field);
600 let jq_path = field_to_jq_path(resolved);
601 let threshold = json_value_to_shell_string(val);
602 let safe_field = sanitize_ident(field);
603 let _ = writeln!(out, " local val_{safe_field}");
604 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
605 let _ = writeln!(
606 out,
607 " assert_greater_than \"$val_{safe_field}\" '{threshold}' '{field}'"
608 );
609 }
610 }
611 }
612 "greater_than_or_equal" => {
613 if let Some(field) = &assertion.field {
614 if let Some(val) = &assertion.value {
615 let resolved = field_resolver.resolve(field);
616 let jq_path = field_to_jq_path(resolved);
617 let threshold = json_value_to_shell_string(val);
618 let safe_field = sanitize_ident(field);
619 let _ = writeln!(out, " local val_{safe_field}");
620 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
621 let _ = writeln!(
622 out,
623 " assert_greater_than_or_equal \"$val_{safe_field}\" '{threshold}' '{field}'"
624 );
625 }
626 }
627 }
628 "contains_all" => {
629 if let Some(field) = &assertion.field {
630 if let Some(serde_json::Value::Array(items)) = &assertion.value {
631 let resolved = field_resolver.resolve(field);
632 let jq_path = field_to_jq_path(resolved);
633 let safe_field = sanitize_ident(field);
634 let _ = writeln!(out, " local val_{safe_field}");
635 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
636 for (index, item) in items.iter().enumerate() {
637 let item_str = json_value_to_shell_string(item);
638 let _ = writeln!(
639 out,
640 " assert_contains \"$val_{safe_field}\" '{item_str}' '{field}[{index}]'"
641 );
642 }
643 }
644 }
645 }
646 "is_empty" => {
647 if let Some(field) = &assertion.field {
648 let resolved = field_resolver.resolve(field);
649 let jq_path = field_to_jq_path(resolved);
650 let safe_field = sanitize_ident(field);
651 let _ = writeln!(out, " local val_{safe_field}");
652 let _ = writeln!(
654 out,
655 " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path} // empty')"
656 );
657 let _ = writeln!(out, " assert_is_empty \"$val_{safe_field}\" '{field}'");
658 }
659 }
660 "less_than" => {
661 if let Some(field) = &assertion.field {
662 if let Some(val) = &assertion.value {
663 let resolved = field_resolver.resolve(field);
664 let jq_path = field_to_jq_path(resolved);
665 let threshold = json_value_to_shell_string(val);
666 let safe_field = sanitize_ident(field);
667 let _ = writeln!(out, " local val_{safe_field}");
668 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
669 let _ = writeln!(
670 out,
671 " assert_less_than \"$val_{safe_field}\" '{threshold}' '{field}'"
672 );
673 }
674 }
675 }
676 "not_contains" => {
677 if let Some(field) = &assertion.field {
678 if let Some(expected) = &assertion.value {
679 let resolved = field_resolver.resolve(field);
680 let jq_path = field_to_jq_path(resolved);
681 let expected_str = json_value_to_shell_string(expected);
682 let safe_field = sanitize_ident(field);
683 let _ = writeln!(out, " local val_{safe_field}");
684 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
685 let _ = writeln!(
686 out,
687 " assert_not_contains \"$val_{safe_field}\" '{expected_str}' '{field}'"
688 );
689 }
690 }
691 }
692 "count_equals" => {
693 if let Some(field) = &assertion.field {
694 if let Some(val) = &assertion.value {
695 if let Some(n) = val.as_u64() {
696 let resolved = field_resolver.resolve(field);
697 let jq_path = field_to_jq_path(resolved);
698 let safe_field = sanitize_ident(field);
699 let _ = writeln!(out, " local count_{safe_field}");
700 let _ = writeln!(
701 out,
702 " count_{safe_field}=$(echo \"$output\" | jq '{jq_path} | length')"
703 );
704 let _ = writeln!(out, " [ \"$count_{safe_field}\" -eq {n} ] || exit 1");
705 }
706 }
707 }
708 }
709 "is_true" => {
710 if let Some(field) = &assertion.field {
711 let resolved = field_resolver.resolve(field);
712 let jq_path = field_to_jq_path(resolved);
713 let safe_field = sanitize_ident(field);
714 let _ = writeln!(out, " local val_{safe_field}");
715 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
716 let _ = writeln!(out, " [ \"$val_{safe_field}\" = \"true\" ] || exit 1");
717 }
718 }
719 "is_false" => {
720 if let Some(field) = &assertion.field {
721 let resolved = field_resolver.resolve(field);
722 let jq_path = field_to_jq_path(resolved);
723 let safe_field = sanitize_ident(field);
724 let _ = writeln!(out, " local val_{safe_field}");
725 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
726 let _ = writeln!(out, " [ \"$val_{safe_field}\" = \"false\" ] || exit 1");
727 }
728 }
729 "less_than_or_equal" => {
730 if let Some(field) = &assertion.field {
731 if let Some(val) = &assertion.value {
732 let resolved = field_resolver.resolve(field);
733 let jq_path = field_to_jq_path(resolved);
734 let threshold = json_value_to_shell_string(val);
735 let safe_field = sanitize_ident(field);
736 let _ = writeln!(out, " local val_{safe_field}");
737 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
738 let _ = writeln!(
739 out,
740 " assert_less_than_or_equal \"$val_{safe_field}\" '{threshold}' '{field}'"
741 );
742 }
743 }
744 }
745 "method_result" => {
746 if let Some(method_name) = &assertion.method {
747 let check = assertion.check.as_deref().unwrap_or("is_true");
748 let cmd = build_brew_method_call(binary_name, method_name, assertion.args.as_ref());
749 if check == "is_error" {
752 let _ = writeln!(out, " if {cmd} >/dev/null 2>&1; then");
753 let _ = writeln!(
754 out,
755 " echo 'FAIL [method_result]: expected method to raise error but it succeeded' >&2"
756 );
757 let _ = writeln!(out, " return 1");
758 let _ = writeln!(out, " fi");
759 } else {
760 let method_var = format!("method_result_{}", sanitize_ident(method_name));
761 let _ = writeln!(out, " local {method_var}");
762 let _ = writeln!(out, " {method_var}=$({cmd})");
763 match check {
764 "equals" => {
765 if let Some(val) = &assertion.value {
766 let expected = json_value_to_shell_string(val);
767 let _ = writeln!(out, " [ \"${method_var}\" = '{expected}' ] || exit 1");
768 }
769 }
770 "is_true" => {
771 let _ = writeln!(out, " [ \"${method_var}\" = \"true\" ] || exit 1");
772 }
773 "is_false" => {
774 let _ = writeln!(out, " [ \"${method_var}\" = \"false\" ] || exit 1");
775 }
776 "greater_than_or_equal" => {
777 if let Some(val) = &assertion.value {
778 if let Some(n) = val.as_u64() {
779 let _ = writeln!(out, " [ \"${method_var}\" -ge {n} ] || exit 1");
780 }
781 }
782 }
783 "count_min" => {
784 if let Some(val) = &assertion.value {
785 if let Some(n) = val.as_u64() {
786 let _ = writeln!(
787 out,
788 " local count_from_method_result=$(echo \"${method_var}\" | jq 'length')"
789 );
790 let _ = writeln!(out, " [ \"$count_from_method_result\" -ge {n} ] || exit 1");
791 }
792 }
793 }
794 "contains" => {
795 if let Some(val) = &assertion.value {
796 let expected = json_value_to_shell_string(val);
797 let _ = writeln!(out, " [[ \"${method_var}\" == *'{expected}'* ]] || exit 1");
798 }
799 }
800 other_check => {
801 panic!("Brew e2e generator: unsupported method_result check type: {other_check}");
802 }
803 }
804 }
805 } else {
806 panic!("method_result assertion missing 'method' field");
807 }
808 }
809 "min_length" => {
810 if let Some(field) = &assertion.field {
811 if let Some(val) = &assertion.value {
812 if let Some(n) = val.as_u64() {
813 let resolved = field_resolver.resolve(field);
814 let jq_path = field_to_jq_path(resolved);
815 let safe_field = sanitize_ident(field);
816 let _ = writeln!(out, " local val_{safe_field}");
817 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
818 let _ = writeln!(
819 out,
820 " [ \"${{#val_{safe_field}}}\" -ge {n} ] || {{ echo \"FAIL [{field}]: expected length >= {n}\" >&2; return 1; }}"
821 );
822 }
823 }
824 }
825 }
826 "max_length" => {
827 if let Some(field) = &assertion.field {
828 if let Some(val) = &assertion.value {
829 if let Some(n) = val.as_u64() {
830 let resolved = field_resolver.resolve(field);
831 let jq_path = field_to_jq_path(resolved);
832 let safe_field = sanitize_ident(field);
833 let _ = writeln!(out, " local val_{safe_field}");
834 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
835 let _ = writeln!(
836 out,
837 " [ \"${{#val_{safe_field}}}\" -le {n} ] || {{ echo \"FAIL [{field}]: expected length <= {n}\" >&2; return 1; }}"
838 );
839 }
840 }
841 }
842 }
843 "ends_with" => {
844 if let Some(field) = &assertion.field {
845 if let Some(expected) = &assertion.value {
846 let resolved = field_resolver.resolve(field);
847 let jq_path = field_to_jq_path(resolved);
848 let expected_str = json_value_to_shell_string(expected);
849 let safe_field = sanitize_ident(field);
850 let _ = writeln!(out, " local val_{safe_field}");
851 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
852 let _ = writeln!(
853 out,
854 " [[ \"$val_{safe_field}\" == *'{expected_str}' ]] || {{ echo \"FAIL [{field}]: expected to end with '{expected_str}'\" >&2; return 1; }}"
855 );
856 }
857 }
858 }
859 "matches_regex" => {
860 if let Some(field) = &assertion.field {
861 if let Some(expected) = &assertion.value {
862 if let Some(pattern) = expected.as_str() {
863 let resolved = field_resolver.resolve(field);
864 let jq_path = field_to_jq_path(resolved);
865 let safe_field = sanitize_ident(field);
866 let _ = writeln!(out, " local val_{safe_field}");
867 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
868 let _ = writeln!(
869 out,
870 " [[ \"$val_{safe_field}\" =~ {pattern} ]] || {{ echo \"FAIL [{field}]: expected to match /{pattern}/\" >&2; return 1; }}"
871 );
872 }
873 }
874 }
875 }
876 "not_error" => {
877 }
879 "error" => {
880 }
882 other => {
883 panic!("Brew e2e generator: unsupported assertion type: {other}");
884 }
885 }
886}
887
888fn json_value_to_shell_string(value: &serde_json::Value) -> String {
892 match value {
893 serde_json::Value::String(s) => escape_shell(s),
894 serde_json::Value::Bool(b) => b.to_string(),
895 serde_json::Value::Number(n) => n.to_string(),
896 serde_json::Value::Null => String::new(),
897 other => escape_shell(&other.to_string()),
898 }
899}