Skip to content

Commit 0e208b0

Browse files
authored
Extract and unify command execution logic (#98)
Created a unified `CommandExecutor` trait to replace specialized executors for `bundler` and `gemset` to simplify the code and prepare the extension for Debugger API.
1 parent 7c84df6 commit 0e208b0

File tree

6 files changed

+155
-150
lines changed

6 files changed

+155
-150
lines changed

src/bundler.rs

Lines changed: 45 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,6 @@
1+
use crate::command_executor::CommandExecutor;
12
use std::path::Path;
2-
use zed_extension_api::{process::Output, Command, Result};
3-
4-
pub trait CommandExecutor {
5-
fn execute_bundle(
6-
&self,
7-
sub_command: String,
8-
args: Vec<String>,
9-
envs: Vec<(String, String)>,
10-
bundle_gemfile_path: &str,
11-
) -> Result<Output>;
12-
}
13-
14-
pub struct RealCommandExecutor;
15-
16-
impl CommandExecutor for RealCommandExecutor {
17-
fn execute_bundle(
18-
&self,
19-
sub_command: String,
20-
args: Vec<String>,
21-
envs: Vec<(String, String)>,
22-
bundle_gemfile_path: &str,
23-
) -> Result<Output> {
24-
Command::new("bundle")
25-
.arg(sub_command)
26-
.args(args)
27-
.envs(envs)
28-
.env("BUNDLE_GEMFILE", bundle_gemfile_path)
29-
.output()
30-
}
31-
}
3+
use zed_extension_api::Result;
324

335
/// A simple wrapper around the `bundle` command.
346
pub struct Bundler {
@@ -74,8 +46,19 @@ impl Bundler {
7446
.to_str()
7547
.ok_or_else(|| "Invalid path to Gemfile".to_string())?;
7648

49+
let full_args: Vec<String> = std::iter::once(cmd).chain(args).collect();
50+
let command_envs: Vec<(String, String)> = self
51+
.envs
52+
.iter()
53+
.cloned()
54+
.chain(std::iter::once((
55+
"BUNDLE_GEMFILE".to_string(),
56+
bundle_gemfile.to_string(),
57+
)))
58+
.collect();
59+
7760
self.command_executor
78-
.execute_bundle(cmd, args, self.envs.clone(), bundle_gemfile)
61+
.execute("bundle", full_args, command_envs)
7962
.and_then(|output| match output.status {
8063
Some(0) => Ok(String::from_utf8_lossy(&output.stdout).to_string()),
8164
Some(status) => {
@@ -96,14 +79,15 @@ impl Bundler {
9679
#[cfg(test)]
9780
mod tests {
9881
use super::*;
82+
use crate::command_executor::CommandExecutor;
9983
use std::cell::RefCell;
84+
use zed_extension_api::process::Output;
10085

10186
struct MockExecutorConfig {
10287
output_to_return: Option<Result<Output>>,
103-
expected_sub_command: Option<String>,
88+
expected_command_name: Option<String>,
10489
expected_args: Option<Vec<String>>,
10590
expected_envs: Option<Vec<(String, String)>>,
106-
expected_bundle_gemfile_path: Option<String>,
10791
}
10892

10993
struct MockCommandExecutor {
@@ -115,64 +99,55 @@ mod tests {
11599
MockCommandExecutor {
116100
config: RefCell::new(MockExecutorConfig {
117101
output_to_return: None,
118-
expected_sub_command: None,
102+
expected_command_name: None,
119103
expected_args: None,
120104
expected_envs: None,
121-
expected_bundle_gemfile_path: None,
122105
}),
123106
}
124107
}
125108

126109
fn expect(
127110
&self,
128-
sub_command: &str,
129-
args: &[&str],
130-
envs: &[(&str, &str)],
131-
bundle_gemfile_path: &str,
132-
output: super::Result<Output>,
111+
command_name: &str,
112+
full_args: &[&str],
113+
final_envs: &[(&str, &str)],
114+
output: Result<Output>,
133115
) {
134116
let mut config = self.config.borrow_mut();
135-
config.expected_sub_command = Some(sub_command.to_string());
136-
config.expected_args = Some(args.iter().map(|s| s.to_string()).collect());
117+
config.expected_command_name = Some(command_name.to_string());
118+
config.expected_args = Some(full_args.iter().map(|s| s.to_string()).collect());
137119
config.expected_envs = Some(
138-
envs.iter()
120+
final_envs
121+
.iter()
139122
.map(|&(k, v)| (k.to_string(), v.to_string()))
140123
.collect(),
141124
);
142-
config.expected_bundle_gemfile_path = Some(bundle_gemfile_path.to_string());
143125
config.output_to_return = Some(output);
144126
}
145127
}
146128

147129
impl CommandExecutor for MockCommandExecutor {
148-
fn execute_bundle(
130+
fn execute(
149131
&self,
150-
sub_command: String,
132+
command_name: &str,
151133
args: Vec<String>,
152134
envs: Vec<(String, String)>,
153-
bundle_gemfile_path: &str,
154-
) -> super::Result<Output> {
135+
) -> Result<Output> {
155136
let mut config = self.config.borrow_mut();
156137

157-
if let Some(expected_cmd) = &config.expected_sub_command {
158-
assert_eq!(&sub_command, expected_cmd, "Mock: Sub-command mismatch");
138+
if let Some(expected_name) = &config.expected_command_name {
139+
assert_eq!(command_name, expected_name, "Mock: Command name mismatch");
159140
}
160141
if let Some(expected_args) = &config.expected_args {
161142
assert_eq!(&args, expected_args, "Mock: Args mismatch");
162143
}
163144
if let Some(expected_envs) = &config.expected_envs {
164145
assert_eq!(&envs, expected_envs, "Mock: Env mismatch");
165146
}
166-
if let Some(expected_path) = &config.expected_bundle_gemfile_path {
167-
assert_eq!(
168-
bundle_gemfile_path, expected_path,
169-
"Mock: Gemfile path mismatch"
170-
);
171-
}
172147

173148
config.output_to_return.take().expect(
174-
"MockCommandExecutor: output_to_return was not set or already consumed for the test",
175-
)
149+
"MockCommandExecutor: output_to_return was not set or already consumed for the test",
150+
)
176151
}
177152
}
178153

@@ -182,11 +157,11 @@ mod tests {
182157
gem: &str,
183158
) -> MockCommandExecutor {
184159
let mock = MockCommandExecutor::new();
160+
let gemfile_path = format!("{}/Gemfile", dir);
185161
mock.expect(
186-
"info",
187-
&["--version", gem],
188-
&[],
189-
&format!("{}/Gemfile", dir),
162+
"bundle",
163+
&["info", "--version", gem],
164+
&[("BUNDLE_GEMFILE", &gemfile_path)],
190165
Ok(Output {
191166
status: Some(0),
192167
stdout: version.as_bytes().to_vec(),
@@ -211,12 +186,12 @@ mod tests {
211186
let mock_executor = MockCommandExecutor::new();
212187
let gem_name = "unknown_gem";
213188
let error_output = "Could not find gem 'unknown_gem'.";
189+
let gemfile_path = "test_dir/Gemfile";
214190

215191
mock_executor.expect(
216-
"info",
217-
&["--version", gem_name],
218-
&[],
219-
"test_dir/Gemfile",
192+
"bundle",
193+
&["info", "--version", gem_name],
194+
&[("BUNDLE_GEMFILE", gemfile_path)],
220195
Ok(Output {
221196
status: Some(1),
222197
stdout: Vec::new(),
@@ -247,12 +222,12 @@ mod tests {
247222
let mock_executor = MockCommandExecutor::new();
248223
let gem_name = "critical_gem";
249224
let specific_error_msg = "Mocked execution failure";
225+
let gemfile_path = "test_dir/Gemfile";
250226

251227
mock_executor.expect(
252-
"info",
253-
&["--version", gem_name],
254-
&[],
255-
"test_dir/Gemfile",
228+
"bundle",
229+
&["info", "--version", gem_name],
230+
&[("BUNDLE_GEMFILE", gemfile_path)],
256231
Err(specific_error_msg.to_string()),
257232
);
258233

src/command_executor.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use zed_extension_api::{process::Output, Command as ZedCommand, Result};
2+
3+
pub trait CommandExecutor {
4+
/// Executes a command with the given arguments and environment variables.
5+
///
6+
/// # Arguments
7+
///
8+
/// * `cmd` - The name or path of the command to execute (e.g., "gem", "bundle").
9+
/// * `args` - A vector of string arguments to pass to the command.
10+
/// * `envs` - A vector of key-value pairs representing environment variables
11+
/// to set for the command's execution context.
12+
///
13+
/// # Returns
14+
///
15+
/// A `Result` containing the `Output` of the command if successful. The `Output`
16+
/// typically includes stdout, stderr, and the exit status. Returns an error
17+
/// if the command execution fails at a lower level (e.g., command not found,
18+
/// or if the `zed_extension_api::Command` itself returns an error).
19+
fn execute(&self, cmd: &str, args: Vec<String>, envs: Vec<(String, String)>) -> Result<Output>;
20+
}
21+
22+
/// An implementation of `CommandExecutor` that executes commands
23+
/// using the `zed_extension_api::Command`.
24+
pub struct RealCommandExecutor;
25+
26+
impl CommandExecutor for RealCommandExecutor {
27+
fn execute(&self, cmd: &str, args: Vec<String>, envs: Vec<(String, String)>) -> Result<Output> {
28+
ZedCommand::new(cmd).args(args).envs(envs).output()
29+
}
30+
}

0 commit comments

Comments
 (0)