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 let upper_id = fixture.id.to_uppercase();
437 parts.push(format!(
438 "\"${{MOCK_SERVER_{upper_id}:-${{MOCK_SERVER_URL}}/fixtures/{}}}\"",
439 fixture.id
440 ));
441 }
442 "handle" => {
443 }
445 _ => {
446 if let Some(flag) = cli_flags.get(&arg.field) {
448 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
449 if let Some(val) = fixture.input.get(field) {
450 if !val.is_null() {
451 let val_str = json_value_to_shell_arg(val);
452 parts.push(flag.clone());
453 parts.push(val_str);
454 }
455 }
456 }
457 }
458 }
459 }
460
461 if let Some(config_val) = fixture.input.get("config") {
463 if !config_val.is_null() {
464 let config_json = serde_json::to_string(config_val).unwrap_or_default();
466 parts.push("--config".to_string());
467 parts.push(format!("'{}'", escape_shell(&config_json)));
468 }
469 }
470
471 for static_arg in static_cli_args {
473 parts.push(static_arg.clone());
474 }
475
476 parts
477}
478
479fn json_value_to_shell_arg(value: &serde_json::Value) -> String {
484 match value {
485 serde_json::Value::String(s) => format!("'{}'", escape_shell(s)),
486 serde_json::Value::Bool(b) => b.to_string(),
487 serde_json::Value::Number(n) => n.to_string(),
488 serde_json::Value::Null => "''".to_string(),
489 other => format!("'{}'", escape_shell(&other.to_string())),
490 }
491}
492
493fn field_to_jq_path(resolved: &str) -> String {
500 if let Some((prefix, suffix)) = resolved.rsplit_once('.') {
503 if suffix == "length" || suffix == "count" || suffix == "size" {
504 return format!(".{prefix} | length");
505 }
506 }
507 if resolved == "length" || resolved == "count" || resolved == "size" {
509 return ". | length".to_string();
510 }
511 format!(".{resolved}")
512}
513
514fn build_brew_method_call(binary_name: &str, method_name: &str, args: Option<&serde_json::Value>) -> String {
521 let subcommand = method_name.replace('_', "-");
522 if let Some(args_val) = args {
523 let arg_str = args_val
524 .as_object()
525 .map(|obj| {
526 obj.values()
527 .map(|v| match v {
528 serde_json::Value::String(s) => format!("'{}'", escape_shell(s)),
529 other => other.to_string(),
530 })
531 .collect::<Vec<_>>()
532 .join(" ")
533 })
534 .unwrap_or_default();
535 if arg_str.is_empty() {
536 format!("{binary_name} {subcommand} \"$output\"")
537 } else {
538 format!("{binary_name} {subcommand} \"$output\" {arg_str}")
539 }
540 } else {
541 format!("{binary_name} {subcommand} \"$output\"")
542 }
543}
544
545fn render_assertion(out: &mut String, assertion: &Assertion, binary_name: &str, field_resolver: &FieldResolver) {
547 if let Some(f) = &assertion.field {
549 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
550 let _ = writeln!(out, " # skipped: field '{f}' not available on result type");
551 return;
552 }
553 }
554
555 match assertion.assertion_type.as_str() {
556 "equals" => {
557 if let Some(field) = &assertion.field {
558 if let Some(expected) = &assertion.value {
559 let resolved = field_resolver.resolve(field);
560 let jq_path = field_to_jq_path(resolved);
561 let expected_str = json_value_to_shell_string(expected);
562 let safe_field = sanitize_ident(field);
563 let _ = writeln!(out, " local val_{safe_field}");
564 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
565 let _ = writeln!(
566 out,
567 " assert_equals \"$val_{safe_field}\" '{expected_str}' '{field}'"
568 );
569 }
570 }
571 }
572 "contains" => {
573 if let Some(field) = &assertion.field {
574 if let Some(expected) = &assertion.value {
575 let resolved = field_resolver.resolve(field);
576 let jq_path = field_to_jq_path(resolved);
577 let expected_str = json_value_to_shell_string(expected);
578 let safe_field = sanitize_ident(field);
579 let _ = writeln!(out, " local val_{safe_field}");
580 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
581 let _ = writeln!(
582 out,
583 " assert_contains \"$val_{safe_field}\" '{expected_str}' '{field}'"
584 );
585 }
586 }
587 }
588 "not_empty" | "tree_not_null" => {
589 if let Some(field) = &assertion.field {
590 let resolved = field_resolver.resolve(field);
591 let jq_path = field_to_jq_path(resolved);
592 let safe_field = sanitize_ident(field);
593 let _ = writeln!(out, " local val_{safe_field}");
594 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
595 let _ = writeln!(out, " assert_not_empty \"$val_{safe_field}\" '{field}'");
596 }
597 }
598 "count_min" | "root_child_count_min" => {
599 if let Some(field) = &assertion.field {
600 if let Some(val) = &assertion.value {
601 if let Some(min) = val.as_u64() {
602 let resolved = field_resolver.resolve(field);
603 let jq_path = field_to_jq_path(resolved);
604 let safe_field = sanitize_ident(field);
605 let _ = writeln!(out, " local count_{safe_field}");
606 let _ = writeln!(
607 out,
608 " count_{safe_field}=$(echo \"$output\" | jq '{jq_path} | length')"
609 );
610 let _ = writeln!(out, " assert_count_min \"$count_{safe_field}\" {min} '{field}'");
611 }
612 }
613 }
614 }
615 "greater_than" => {
616 if let Some(field) = &assertion.field {
617 if let Some(val) = &assertion.value {
618 let resolved = field_resolver.resolve(field);
619 let jq_path = field_to_jq_path(resolved);
620 let threshold = json_value_to_shell_string(val);
621 let safe_field = sanitize_ident(field);
622 let _ = writeln!(out, " local val_{safe_field}");
623 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
624 let _ = writeln!(
625 out,
626 " assert_greater_than \"$val_{safe_field}\" '{threshold}' '{field}'"
627 );
628 }
629 }
630 }
631 "greater_than_or_equal" => {
632 if let Some(field) = &assertion.field {
633 if let Some(val) = &assertion.value {
634 let resolved = field_resolver.resolve(field);
635 let jq_path = field_to_jq_path(resolved);
636 let threshold = json_value_to_shell_string(val);
637 let safe_field = sanitize_ident(field);
638 let _ = writeln!(out, " local val_{safe_field}");
639 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
640 let _ = writeln!(
641 out,
642 " assert_greater_than_or_equal \"$val_{safe_field}\" '{threshold}' '{field}'"
643 );
644 }
645 }
646 }
647 "contains_all" => {
648 if let Some(field) = &assertion.field {
649 if let Some(serde_json::Value::Array(items)) = &assertion.value {
650 let resolved = field_resolver.resolve(field);
651 let jq_path = field_to_jq_path(resolved);
652 let safe_field = sanitize_ident(field);
653 let _ = writeln!(out, " local val_{safe_field}");
654 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
655 for (index, item) in items.iter().enumerate() {
656 let item_str = json_value_to_shell_string(item);
657 let _ = writeln!(
658 out,
659 " assert_contains \"$val_{safe_field}\" '{item_str}' '{field}[{index}]'"
660 );
661 }
662 }
663 }
664 }
665 "is_empty" => {
666 if let Some(field) = &assertion.field {
667 let resolved = field_resolver.resolve(field);
668 let jq_path = field_to_jq_path(resolved);
669 let safe_field = sanitize_ident(field);
670 let _ = writeln!(out, " local val_{safe_field}");
671 let _ = writeln!(
673 out,
674 " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path} // empty')"
675 );
676 let _ = writeln!(out, " assert_is_empty \"$val_{safe_field}\" '{field}'");
677 }
678 }
679 "less_than" => {
680 if let Some(field) = &assertion.field {
681 if let Some(val) = &assertion.value {
682 let resolved = field_resolver.resolve(field);
683 let jq_path = field_to_jq_path(resolved);
684 let threshold = json_value_to_shell_string(val);
685 let safe_field = sanitize_ident(field);
686 let _ = writeln!(out, " local val_{safe_field}");
687 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
688 let _ = writeln!(
689 out,
690 " assert_less_than \"$val_{safe_field}\" '{threshold}' '{field}'"
691 );
692 }
693 }
694 }
695 "not_contains" => {
696 if let Some(field) = &assertion.field {
697 if let Some(expected) = &assertion.value {
698 let resolved = field_resolver.resolve(field);
699 let jq_path = field_to_jq_path(resolved);
700 let expected_str = json_value_to_shell_string(expected);
701 let safe_field = sanitize_ident(field);
702 let _ = writeln!(out, " local val_{safe_field}");
703 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
704 let _ = writeln!(
705 out,
706 " assert_not_contains \"$val_{safe_field}\" '{expected_str}' '{field}'"
707 );
708 }
709 }
710 }
711 "count_equals" => {
712 if let Some(field) = &assertion.field {
713 if let Some(val) = &assertion.value {
714 if let Some(n) = val.as_u64() {
715 let resolved = field_resolver.resolve(field);
716 let jq_path = field_to_jq_path(resolved);
717 let safe_field = sanitize_ident(field);
718 let _ = writeln!(out, " local count_{safe_field}");
719 let _ = writeln!(
720 out,
721 " count_{safe_field}=$(echo \"$output\" | jq '{jq_path} | length')"
722 );
723 let _ = writeln!(out, " [ \"$count_{safe_field}\" -eq {n} ] || exit 1");
724 }
725 }
726 }
727 }
728 "is_true" => {
729 if let Some(field) = &assertion.field {
730 let resolved = field_resolver.resolve(field);
731 let jq_path = field_to_jq_path(resolved);
732 let safe_field = sanitize_ident(field);
733 let _ = writeln!(out, " local val_{safe_field}");
734 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
735 let _ = writeln!(out, " [ \"$val_{safe_field}\" = \"true\" ] || exit 1");
736 }
737 }
738 "is_false" => {
739 if let Some(field) = &assertion.field {
740 let resolved = field_resolver.resolve(field);
741 let jq_path = field_to_jq_path(resolved);
742 let safe_field = sanitize_ident(field);
743 let _ = writeln!(out, " local val_{safe_field}");
744 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
745 let _ = writeln!(out, " [ \"$val_{safe_field}\" = \"false\" ] || exit 1");
746 }
747 }
748 "less_than_or_equal" => {
749 if let Some(field) = &assertion.field {
750 if let Some(val) = &assertion.value {
751 let resolved = field_resolver.resolve(field);
752 let jq_path = field_to_jq_path(resolved);
753 let threshold = json_value_to_shell_string(val);
754 let safe_field = sanitize_ident(field);
755 let _ = writeln!(out, " local val_{safe_field}");
756 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
757 let _ = writeln!(
758 out,
759 " assert_less_than_or_equal \"$val_{safe_field}\" '{threshold}' '{field}'"
760 );
761 }
762 }
763 }
764 "method_result" => {
765 if let Some(method_name) = &assertion.method {
766 let check = assertion.check.as_deref().unwrap_or("is_true");
767 let cmd = build_brew_method_call(binary_name, method_name, assertion.args.as_ref());
768 if check == "is_error" {
771 let _ = writeln!(out, " if {cmd} >/dev/null 2>&1; then");
772 let _ = writeln!(
773 out,
774 " echo 'FAIL [method_result]: expected method to raise error but it succeeded' >&2"
775 );
776 let _ = writeln!(out, " return 1");
777 let _ = writeln!(out, " fi");
778 } else {
779 let method_var = format!("method_result_{}", sanitize_ident(method_name));
780 let _ = writeln!(out, " local {method_var}");
781 let _ = writeln!(out, " {method_var}=$({cmd})");
782 match check {
783 "equals" => {
784 if let Some(val) = &assertion.value {
785 let expected = json_value_to_shell_string(val);
786 let _ = writeln!(out, " [ \"${method_var}\" = '{expected}' ] || exit 1");
787 }
788 }
789 "is_true" => {
790 let _ = writeln!(out, " [ \"${method_var}\" = \"true\" ] || exit 1");
791 }
792 "is_false" => {
793 let _ = writeln!(out, " [ \"${method_var}\" = \"false\" ] || exit 1");
794 }
795 "greater_than_or_equal" => {
796 if let Some(val) = &assertion.value {
797 if let Some(n) = val.as_u64() {
798 let _ = writeln!(out, " [ \"${method_var}\" -ge {n} ] || exit 1");
799 }
800 }
801 }
802 "count_min" => {
803 if let Some(val) = &assertion.value {
804 if let Some(n) = val.as_u64() {
805 let _ = writeln!(
806 out,
807 " local count_from_method_result=$(echo \"${method_var}\" | jq 'length')"
808 );
809 let _ = writeln!(out, " [ \"$count_from_method_result\" -ge {n} ] || exit 1");
810 }
811 }
812 }
813 "contains" => {
814 if let Some(val) = &assertion.value {
815 let expected = json_value_to_shell_string(val);
816 let _ = writeln!(out, " [[ \"${method_var}\" == *'{expected}'* ]] || exit 1");
817 }
818 }
819 other_check => {
820 panic!("Brew e2e generator: unsupported method_result check type: {other_check}");
821 }
822 }
823 }
824 } else {
825 panic!("method_result assertion missing 'method' field");
826 }
827 }
828 "min_length" => {
829 if let Some(field) = &assertion.field {
830 if let Some(val) = &assertion.value {
831 if let Some(n) = val.as_u64() {
832 let resolved = field_resolver.resolve(field);
833 let jq_path = field_to_jq_path(resolved);
834 let safe_field = sanitize_ident(field);
835 let _ = writeln!(out, " local val_{safe_field}");
836 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
837 let _ = writeln!(
838 out,
839 " [ \"${{#val_{safe_field}}}\" -ge {n} ] || {{ echo \"FAIL [{field}]: expected length >= {n}\" >&2; return 1; }}"
840 );
841 }
842 }
843 }
844 }
845 "max_length" => {
846 if let Some(field) = &assertion.field {
847 if let Some(val) = &assertion.value {
848 if let Some(n) = val.as_u64() {
849 let resolved = field_resolver.resolve(field);
850 let jq_path = field_to_jq_path(resolved);
851 let safe_field = sanitize_ident(field);
852 let _ = writeln!(out, " local val_{safe_field}");
853 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
854 let _ = writeln!(
855 out,
856 " [ \"${{#val_{safe_field}}}\" -le {n} ] || {{ echo \"FAIL [{field}]: expected length <= {n}\" >&2; return 1; }}"
857 );
858 }
859 }
860 }
861 }
862 "ends_with" => {
863 if let Some(field) = &assertion.field {
864 if let Some(expected) = &assertion.value {
865 let resolved = field_resolver.resolve(field);
866 let jq_path = field_to_jq_path(resolved);
867 let expected_str = json_value_to_shell_string(expected);
868 let safe_field = sanitize_ident(field);
869 let _ = writeln!(out, " local val_{safe_field}");
870 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
871 let _ = writeln!(
872 out,
873 " [[ \"$val_{safe_field}\" == *'{expected_str}' ]] || {{ echo \"FAIL [{field}]: expected to end with '{expected_str}'\" >&2; return 1; }}"
874 );
875 }
876 }
877 }
878 "matches_regex" => {
879 if let Some(field) = &assertion.field {
880 if let Some(expected) = &assertion.value {
881 if let Some(pattern) = expected.as_str() {
882 let resolved = field_resolver.resolve(field);
883 let jq_path = field_to_jq_path(resolved);
884 let safe_field = sanitize_ident(field);
885 let _ = writeln!(out, " local val_{safe_field}");
886 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
887 let _ = writeln!(
888 out,
889 " [[ \"$val_{safe_field}\" =~ {pattern} ]] || {{ echo \"FAIL [{field}]: expected to match /{pattern}/\" >&2; return 1; }}"
890 );
891 }
892 }
893 }
894 }
895 "not_error" => {
896 }
898 "error" => {
899 }
901 other => {
902 panic!("Brew e2e generator: unsupported assertion type: {other}");
903 }
904 }
905}
906
907fn json_value_to_shell_string(value: &serde_json::Value) -> String {
911 match value {
912 serde_json::Value::String(s) => escape_shell(s),
913 serde_json::Value::Bool(b) => b.to_string(),
914 serde_json::Value::Number(n) => n.to_string(),
915 serde_json::Value::Null => String::new(),
916 other => escape_shell(&other.to_string()),
917 }
918}