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