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