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