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, 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(method_name: &str, args: Option<&serde_json::Value>) -> String {
499 match method_name {
500 "root_child_count" => "tree_sitter_language_pack tree-root-child-count \"$output\"".to_string(),
501 "root_node_type" => "tree_sitter_language_pack tree-root-node-type \"$output\"".to_string(),
502 "named_children_count" => "tree_sitter_language_pack tree-named-children-count \"$output\"".to_string(),
503 "has_error_nodes" => "tree_sitter_language_pack tree-has-error-nodes \"$output\"".to_string(),
504 "error_count" | "tree_error_count" => "tree_sitter_language_pack tree-error-count \"$output\"".to_string(),
505 "tree_to_sexp" => "tree_sitter_language_pack tree-to-sexp \"$output\"".to_string(),
506 "contains_node_type" => {
507 let node_type = args
508 .and_then(|a| a.get("node_type"))
509 .and_then(|v| v.as_str())
510 .unwrap_or("");
511 format!("tree_sitter_language_pack tree-contains-node-type \"$output\" '{node_type}'")
512 }
513 "find_nodes_by_type" => {
514 let node_type = args
515 .and_then(|a| a.get("node_type"))
516 .and_then(|v| v.as_str())
517 .unwrap_or("");
518 format!("tree_sitter_language_pack tree-find-nodes-by-type \"$output\" '{node_type}'")
519 }
520 "run_query" => {
521 let query_source = args
522 .and_then(|a| a.get("query_source"))
523 .and_then(|v| v.as_str())
524 .unwrap_or("");
525 let language = args
526 .and_then(|a| a.get("language"))
527 .and_then(|v| v.as_str())
528 .unwrap_or("");
529 format!("tree_sitter_language_pack tree-run-query \"$output\" '{language}' '{query_source}'")
530 }
531 _ => {
532 if let Some(args_val) = args {
533 let arg_str = args_val
534 .as_object()
535 .map(|obj| {
536 obj.iter()
537 .map(|(k, v)| {
538 let val_str = match v {
539 serde_json::Value::String(s) => format!("'{}'", escape_shell(s)),
540 other => other.to_string(),
541 };
542 format!("--{k} {val_str}")
543 })
544 .collect::<Vec<_>>()
545 .join(" ")
546 })
547 .unwrap_or_default();
548 format!("tree_sitter_language_pack {method_name} \"$output\" {arg_str}")
549 } else {
550 format!("tree_sitter_language_pack {method_name} \"$output\"")
551 }
552 }
553 }
554}
555
556fn render_assertion(out: &mut String, assertion: &Assertion, field_resolver: &FieldResolver) {
558 if let Some(f) = &assertion.field {
560 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
561 let _ = writeln!(out, " # skipped: field '{f}' not available on result type");
562 return;
563 }
564 }
565
566 match assertion.assertion_type.as_str() {
567 "equals" => {
568 if let Some(field) = &assertion.field {
569 if let Some(expected) = &assertion.value {
570 let resolved = field_resolver.resolve(field);
571 let jq_path = field_to_jq_path(resolved);
572 let expected_str = json_value_to_shell_string(expected);
573 let safe_field = sanitize_ident(field);
574 let _ = writeln!(out, " local val_{safe_field}");
575 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
576 let _ = writeln!(
577 out,
578 " assert_equals \"$val_{safe_field}\" '{expected_str}' '{field}'"
579 );
580 }
581 }
582 }
583 "contains" => {
584 if let Some(field) = &assertion.field {
585 if let Some(expected) = &assertion.value {
586 let resolved = field_resolver.resolve(field);
587 let jq_path = field_to_jq_path(resolved);
588 let expected_str = json_value_to_shell_string(expected);
589 let safe_field = sanitize_ident(field);
590 let _ = writeln!(out, " local val_{safe_field}");
591 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
592 let _ = writeln!(
593 out,
594 " assert_contains \"$val_{safe_field}\" '{expected_str}' '{field}'"
595 );
596 }
597 }
598 }
599 "not_empty" | "tree_not_null" => {
600 if let Some(field) = &assertion.field {
601 let resolved = field_resolver.resolve(field);
602 let jq_path = field_to_jq_path(resolved);
603 let safe_field = sanitize_ident(field);
604 let _ = writeln!(out, " local val_{safe_field}");
605 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
606 let _ = writeln!(out, " assert_not_empty \"$val_{safe_field}\" '{field}'");
607 }
608 }
609 "count_min" | "root_child_count_min" => {
610 if let Some(field) = &assertion.field {
611 if let Some(val) = &assertion.value {
612 if let Some(min) = val.as_u64() {
613 let resolved = field_resolver.resolve(field);
614 let jq_path = field_to_jq_path(resolved);
615 let safe_field = sanitize_ident(field);
616 let _ = writeln!(out, " local count_{safe_field}");
617 let _ = writeln!(
618 out,
619 " count_{safe_field}=$(echo \"$output\" | jq '{jq_path} | length')"
620 );
621 let _ = writeln!(out, " assert_count_min \"$count_{safe_field}\" {min} '{field}'");
622 }
623 }
624 }
625 }
626 "greater_than" => {
627 if let Some(field) = &assertion.field {
628 if let Some(val) = &assertion.value {
629 let resolved = field_resolver.resolve(field);
630 let jq_path = field_to_jq_path(resolved);
631 let threshold = json_value_to_shell_string(val);
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 let _ = writeln!(
636 out,
637 " assert_greater_than \"$val_{safe_field}\" '{threshold}' '{field}'"
638 );
639 }
640 }
641 }
642 "greater_than_or_equal" => {
643 if let Some(field) = &assertion.field {
644 if let Some(val) = &assertion.value {
645 let resolved = field_resolver.resolve(field);
646 let jq_path = field_to_jq_path(resolved);
647 let threshold = json_value_to_shell_string(val);
648 let safe_field = sanitize_ident(field);
649 let _ = writeln!(out, " local val_{safe_field}");
650 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
651 let _ = writeln!(
652 out,
653 " assert_greater_than_or_equal \"$val_{safe_field}\" '{threshold}' '{field}'"
654 );
655 }
656 }
657 }
658 "contains_all" => {
659 if let Some(field) = &assertion.field {
660 if let Some(serde_json::Value::Array(items)) = &assertion.value {
661 let resolved = field_resolver.resolve(field);
662 let jq_path = field_to_jq_path(resolved);
663 let safe_field = sanitize_ident(field);
664 let _ = writeln!(out, " local val_{safe_field}");
665 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
666 for (index, item) in items.iter().enumerate() {
667 let item_str = json_value_to_shell_string(item);
668 let _ = writeln!(
669 out,
670 " assert_contains \"$val_{safe_field}\" '{item_str}' '{field}[{index}]'"
671 );
672 }
673 }
674 }
675 }
676 "is_empty" => {
677 if let Some(field) = &assertion.field {
678 let resolved = field_resolver.resolve(field);
679 let jq_path = field_to_jq_path(resolved);
680 let safe_field = sanitize_ident(field);
681 let _ = writeln!(out, " local val_{safe_field}");
682 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
683 let _ = writeln!(out, " assert_is_empty \"$val_{safe_field}\" '{field}'");
684 }
685 }
686 "less_than" => {
687 if let Some(field) = &assertion.field {
688 if let Some(val) = &assertion.value {
689 let resolved = field_resolver.resolve(field);
690 let jq_path = field_to_jq_path(resolved);
691 let threshold = json_value_to_shell_string(val);
692 let safe_field = sanitize_ident(field);
693 let _ = writeln!(out, " local val_{safe_field}");
694 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
695 let _ = writeln!(
696 out,
697 " assert_less_than \"$val_{safe_field}\" '{threshold}' '{field}'"
698 );
699 }
700 }
701 }
702 "not_contains" => {
703 if let Some(field) = &assertion.field {
704 if let Some(expected) = &assertion.value {
705 let resolved = field_resolver.resolve(field);
706 let jq_path = field_to_jq_path(resolved);
707 let expected_str = json_value_to_shell_string(expected);
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!(
712 out,
713 " assert_not_contains \"$val_{safe_field}\" '{expected_str}' '{field}'"
714 );
715 }
716 }
717 }
718 "count_equals" => {
719 if let Some(field) = &assertion.field {
720 if let Some(val) = &assertion.value {
721 if let Some(n) = val.as_u64() {
722 let resolved = field_resolver.resolve(field);
723 let jq_path = field_to_jq_path(resolved);
724 let safe_field = sanitize_ident(field);
725 let _ = writeln!(out, " local count_{safe_field}");
726 let _ = writeln!(
727 out,
728 " count_{safe_field}=$(echo \"$output\" | jq '{jq_path} | length')"
729 );
730 let _ = writeln!(out, " [ \"$count_{safe_field}\" -eq {n} ] || exit 1");
731 }
732 }
733 }
734 }
735 "is_true" => {
736 if let Some(field) = &assertion.field {
737 let resolved = field_resolver.resolve(field);
738 let jq_path = field_to_jq_path(resolved);
739 let safe_field = sanitize_ident(field);
740 let _ = writeln!(out, " local val_{safe_field}");
741 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
742 let _ = writeln!(out, " [ \"$val_{safe_field}\" = \"true\" ] || exit 1");
743 }
744 }
745 "is_false" => {
746 if let Some(field) = &assertion.field {
747 let resolved = field_resolver.resolve(field);
748 let jq_path = field_to_jq_path(resolved);
749 let safe_field = sanitize_ident(field);
750 let _ = writeln!(out, " local val_{safe_field}");
751 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
752 let _ = writeln!(out, " [ \"$val_{safe_field}\" = \"false\" ] || exit 1");
753 }
754 }
755 "less_than_or_equal" => {
756 if let Some(field) = &assertion.field {
757 if let Some(val) = &assertion.value {
758 let resolved = field_resolver.resolve(field);
759 let jq_path = field_to_jq_path(resolved);
760 let threshold = json_value_to_shell_string(val);
761 let safe_field = sanitize_ident(field);
762 let _ = writeln!(out, " local val_{safe_field}");
763 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
764 let _ = writeln!(
765 out,
766 " assert_less_than_or_equal \"$val_{safe_field}\" '{threshold}' '{field}'"
767 );
768 }
769 }
770 }
771 "method_result" => {
772 if let Some(method_name) = &assertion.method {
773 let check = assertion.check.as_deref().unwrap_or("is_true");
774 let cmd = build_brew_method_call(method_name, assertion.args.as_ref());
775 if check == "is_error" {
778 let _ = writeln!(out, " if {cmd} >/dev/null 2>&1; then");
779 let _ = writeln!(
780 out,
781 " echo 'FAIL [method_result]: expected method to raise error but it succeeded' >&2"
782 );
783 let _ = writeln!(out, " return 1");
784 let _ = writeln!(out, " fi");
785 } else {
786 let method_var = format!("method_result_{}", sanitize_ident(method_name));
787 let _ = writeln!(out, " local {method_var}");
788 let _ = writeln!(out, " {method_var}=$({cmd})");
789 match check {
790 "equals" => {
791 if let Some(val) = &assertion.value {
792 let expected = json_value_to_shell_string(val);
793 let _ = writeln!(out, " [ \"${method_var}\" = '{expected}' ] || exit 1");
794 }
795 }
796 "is_true" => {
797 let _ = writeln!(out, " [ \"${method_var}\" = \"true\" ] || exit 1");
798 }
799 "is_false" => {
800 let _ = writeln!(out, " [ \"${method_var}\" = \"false\" ] || exit 1");
801 }
802 "greater_than_or_equal" => {
803 if let Some(val) = &assertion.value {
804 if let Some(n) = val.as_u64() {
805 let _ = writeln!(out, " [ \"${method_var}\" -ge {n} ] || exit 1");
806 }
807 }
808 }
809 "count_min" => {
810 if let Some(val) = &assertion.value {
811 if let Some(n) = val.as_u64() {
812 let _ = writeln!(
813 out,
814 " local count_from_method_result=$(echo \"${method_var}\" | jq 'length')"
815 );
816 let _ = writeln!(out, " [ \"$count_from_method_result\" -ge {n} ] || exit 1");
817 }
818 }
819 }
820 "contains" => {
821 if let Some(val) = &assertion.value {
822 let expected = json_value_to_shell_string(val);
823 let _ = writeln!(out, " [[ \"${method_var}\" == *'{expected}'* ]] || exit 1");
824 }
825 }
826 other_check => {
827 panic!("Brew e2e generator: unsupported method_result check type: {other_check}");
828 }
829 }
830 }
831 } else {
832 panic!("method_result assertion missing 'method' field");
833 }
834 }
835 "min_length" => {
836 if let Some(field) = &assertion.field {
837 if let Some(val) = &assertion.value {
838 if let Some(n) = val.as_u64() {
839 let resolved = field_resolver.resolve(field);
840 let jq_path = field_to_jq_path(resolved);
841 let safe_field = sanitize_ident(field);
842 let _ = writeln!(out, " local val_{safe_field}");
843 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
844 let _ = writeln!(
845 out,
846 " [ \"${{#val_{safe_field}}}\" -ge {n} ] || {{ echo \"FAIL [{field}]: expected length >= {n}\" >&2; return 1; }}"
847 );
848 }
849 }
850 }
851 }
852 "max_length" => {
853 if let Some(field) = &assertion.field {
854 if let Some(val) = &assertion.value {
855 if let Some(n) = val.as_u64() {
856 let resolved = field_resolver.resolve(field);
857 let jq_path = field_to_jq_path(resolved);
858 let safe_field = sanitize_ident(field);
859 let _ = writeln!(out, " local val_{safe_field}");
860 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
861 let _ = writeln!(
862 out,
863 " [ \"${{#val_{safe_field}}}\" -le {n} ] || {{ echo \"FAIL [{field}]: expected length <= {n}\" >&2; return 1; }}"
864 );
865 }
866 }
867 }
868 }
869 "ends_with" => {
870 if let Some(field) = &assertion.field {
871 if let Some(expected) = &assertion.value {
872 let resolved = field_resolver.resolve(field);
873 let jq_path = field_to_jq_path(resolved);
874 let expected_str = json_value_to_shell_string(expected);
875 let safe_field = sanitize_ident(field);
876 let _ = writeln!(out, " local val_{safe_field}");
877 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
878 let _ = writeln!(
879 out,
880 " [[ \"$val_{safe_field}\" == *'{expected_str}' ]] || {{ echo \"FAIL [{field}]: expected to end with '{expected_str}'\" >&2; return 1; }}"
881 );
882 }
883 }
884 }
885 "matches_regex" => {
886 if let Some(field) = &assertion.field {
887 if let Some(expected) = &assertion.value {
888 if let Some(pattern) = expected.as_str() {
889 let resolved = field_resolver.resolve(field);
890 let jq_path = field_to_jq_path(resolved);
891 let safe_field = sanitize_ident(field);
892 let _ = writeln!(out, " local val_{safe_field}");
893 let _ = writeln!(out, " val_{safe_field}=$(echo \"$output\" | jq -r '{jq_path}')");
894 let _ = writeln!(
895 out,
896 " [[ \"$val_{safe_field}\" =~ {pattern} ]] || {{ echo \"FAIL [{field}]: expected to match /{pattern}/\" >&2; return 1; }}"
897 );
898 }
899 }
900 }
901 }
902 "not_error" => {
903 }
905 "error" => {
906 }
908 other => {
909 panic!("Brew e2e generator: unsupported assertion type: {other}");
910 }
911 }
912}
913
914fn json_value_to_shell_string(value: &serde_json::Value) -> String {
918 match value {
919 serde_json::Value::String(s) => escape_shell(s),
920 serde_json::Value::Bool(b) => b.to_string(),
921 serde_json::Value::Number(n) => n.to_string(),
922 serde_json::Value::Null => String::new(),
923 other => escape_shell(&other.to_string()),
924 }
925}