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