1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Brew (Homebrew CLI) e2e test generator.
//!
//! Generates a self-contained shell-script test suite that tests a CLI binary
//! installed via Homebrew. The suite consists of:
//!
//! - `run_tests.sh` — main runner that sources per-category files, tracks
//! pass/fail counts and exits 1 on any failure.
//! - `test_{category}.sh` — one file per fixture category, each containing
//! a `test_{fixture_id}()` shell function.
//!
//! Each test function:
//! 1. Constructs a CLI invocation: `{binary} {subcommand} "{url}" {flags...}`
//! 2. Captures stdout into a variable.
//! 3. Uses `jq` to extract fields and runs helper assertion functions.
//!
//! Requirements at runtime: `bash`, `jq`, and `MOCK_SERVER_URL` env var.
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::sanitize_filename;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use anyhow::Result;
use std::path::PathBuf;
use super::E2eCodegen;
mod category;
mod run_tests;
use category::render_category_file;
use run_tests::render_run_tests;
/// Brew (Homebrew CLI) e2e code generator.
pub struct BrewCodegen;
impl E2eCodegen for BrewCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
_config: &ResolvedCrateConfig,
_type_defs: &[crate::core::ir::TypeDef],
_enums: &[crate::core::ir::EnumDef],
_functions: &[crate::core::ir::FunctionDef],
// ~keep Unused on purpose: brew keeps its own single combined-output `grep -F` check
// rather than routing through `declared_error_variant::classify`, because a CLI's error
// text is the only observable signal it has. See that module's doc for the rationale.
_errors: &[crate::core::ir::ErrorDef],
) -> Result<Vec<GeneratedFile>> {
let lang = self.language_name();
let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
// Resolve call config with overrides for the "brew" language key.
let call = &e2e_config.call;
let overrides = call.overrides.get(lang);
// Default subcommand (used when fixture has no routing tags).
let default_subcommand = overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call.function.clone());
// Static CLI flags appended to every invocation.
let static_cli_args: Vec<String> = overrides.map(|o| o.cli_args.clone()).unwrap_or_default();
// Field-to-flag mapping (fixture input field → CLI flag name).
let cli_flags: std::collections::HashMap<String, String> =
overrides.map(|o| o.cli_flags.clone()).unwrap_or_default();
// Resolve binary name from the "brew" package entry, falling back to call.module.
let binary_name = e2e_config
.registry
.packages
.get(lang)
.and_then(|p| p.name.as_ref())
.cloned()
.or_else(|| e2e_config.packages.get(lang).and_then(|p| p.name.as_ref()).cloned())
.unwrap_or_else(|| call.module.clone());
// Filter active groups (non-skipped fixtures).
let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
.iter()
.filter_map(|group| {
let active: Vec<&Fixture> = group
.fixtures
.iter()
.filter(|f| super::should_include_fixture(f, lang, e2e_config))
.collect();
if active.is_empty() { None } else { Some((group, active)) }
})
.collect();
let mut files = Vec::new();
// Generate run_tests.sh.
let category_names: Vec<String> = active_groups
.iter()
.map(|(g, _)| sanitize_filename(&g.category))
.collect();
files.push(GeneratedFile {
path: output_base.join("run_tests.sh"),
content: render_run_tests(&category_names, &e2e_config.env, &binary_name),
generated_header: true,
});
// Generate per-category test files.
for (group, active) in &active_groups {
let safe_category = sanitize_filename(&group.category);
let filename = format!("test_{safe_category}.sh");
let content = render_category_file(
&group.category,
active,
&binary_name,
&default_subcommand,
&static_cli_args,
&cli_flags,
&e2e_config.call.args,
e2e_config,
);
files.push(GeneratedFile {
path: output_base.join(filename),
content,
generated_header: true,
});
}
Ok(files)
}
fn language_name(&self) -> &'static str {
"brew"
}
}
/// Emit a brew test backend stub.
///
/// Brew is a packaging-verification backend with no `test_backend` stub
/// generator. Panic rather than return a placeholder `TestBackendEmission` a
/// caller could accidentally splice into generated code. ~keep
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
_methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
) -> super::TestBackendEmission {
panic!(
"brew e2e generator: fixture `{}` requires a brew test_backend stub for trait `{}`, but the brew test-backend emitter is unimplemented; refusing to emit a call with a comment where the argument belongs",
fixture.id, trait_bridge.trait_name
);
}