Skip to content

Commit 2c46b13

Browse files
committed
Merge #218: refactor: extract private methods from run_command for single responsibility
acf3e5e refactor: extract private methods from run_command for single responsibility (Jose Celano) 15896eb refactor: extract working directory validation into guard clause method (Jose Celano) Pull request description: ## Summary Refactors the `run_command` method in `CommandExecutor` to improve readability and maintainability by extracting helper methods, each with a single responsibility. ## Changes The `run_command` method was too large with multiple abstraction levels mixed together. This PR extracts the following private helper methods: - **`validate_working_directory`** - Guard clause for directory existence validation - **`build_command`** - Constructs `Command` with args and working directory - **`format_command_display`** - Formats command string for logs/error messages - **`log_command_start`** - Logs command execution start with tracing - **`execute_command`** - Runs the command and returns `(ExitStatus, stdout, stderr)` - **`extract_output`** - Converts raw output bytes to strings - **`check_command_success`** - Validates exit status and returns error if failed - **`log_command_output`** - Logs stdout/stderr at debug level ## Result The `run_command` method now reads as a clean, high-level workflow: ```rust pub fn run_command(...) -> Result<CommandResult, CommandError> { Self::validate_working_directory(working_dir)?; let mut command = Self::build_command(cmd, args, working_dir); let command_display = Self::format_command_display(cmd, args); Self::log_command_start(&command_display, working_dir); let (status, stdout, stderr) = Self::execute_command(&mut command, &command_display)?; Self::check_command_success(status, &command_display, &stdout, &stderr)?; Self::log_command_output(&command_display, &stdout, &stderr); Ok(CommandResult::new(status, stdout, stderr)) } ``` Each step is at the same abstraction level, making the code easier to understand and maintain. ## Testing - All existing unit tests pass (7 tests in executor module) - All 1250+ tests in the project pass - E2E tests pass ACKs for top commit: josecelano: ACK acf3e5e Tree-SHA512: f85e12f1e5b95981a965dd930c638abe44915901aa1cefdb223f657039449bb43cf6e8620b2abc53bfa06007ac459c0480c54f53970483e68d3dbd04d62a0cc6
2 parents 38cc5c9 + acf3e5e commit 2c46b13

File tree

1 file changed

+73
-14
lines changed

1 file changed

+73
-14
lines changed

src/shared/command/executor.rs

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,28 @@ impl CommandExecutor {
4949
args: &[&str],
5050
working_dir: Option<&Path>,
5151
) -> Result<CommandResult, CommandError> {
52-
// Check if working directory exists before attempting to run the command
53-
// This provides a clearer error message than the generic "No such file or directory"
52+
Self::validate_working_directory(working_dir)?;
53+
54+
let mut command = Self::build_command(cmd, args, working_dir);
55+
56+
let command_display = Self::format_command_display(cmd, args);
57+
58+
Self::log_command_start(&command_display, working_dir);
59+
60+
let (status, stdout, stderr) = Self::execute_command(&mut command, &command_display)?;
61+
62+
Self::check_command_success(status, &command_display, &stdout, &stderr)?;
63+
64+
Self::log_command_output(&command_display, &stdout, &stderr);
65+
66+
Ok(CommandResult::new(status, stdout, stderr))
67+
}
68+
69+
/// Validates that the working directory exists if provided.
70+
///
71+
/// This provides a clearer error message than the generic "No such file or directory"
72+
/// that would be returned by the OS.
73+
fn validate_working_directory(working_dir: Option<&Path>) -> Result<(), CommandError> {
5474
if let Some(dir) = working_dir {
5575
if !dir.exists() {
5676
return Err(CommandError::WorkingDirectoryNotFound {
@@ -59,55 +79,96 @@ impl CommandExecutor {
5979
}
6080
}
6181

82+
Ok(())
83+
}
84+
85+
/// Builds a Command with the given arguments and optional working directory.
86+
fn build_command(cmd: &str, args: &[&str], working_dir: Option<&Path>) -> Command {
6287
let mut command = Command::new(cmd);
63-
let command_display = format!("{} {}", cmd, args.join(" "));
6488

6589
command.args(args);
6690

6791
if let Some(dir) = working_dir {
6892
command.current_dir(dir);
6993
}
7094

95+
command
96+
}
97+
98+
/// Formats a command and its arguments for display in logs and error messages.
99+
fn format_command_display(cmd: &str, args: &[&str]) -> String {
100+
format!("{} {}", cmd, args.join(" "))
101+
}
102+
103+
/// Logs the command execution start with optional working directory.
104+
fn log_command_start(command_display: &str, working_dir: Option<&Path>) {
71105
info!(
72106
operation = "command_execution",
73107
command = %command_display,
74108
"Running command"
75109
);
110+
76111
if let Some(dir) = working_dir {
77112
info!(
78113
operation = "command_execution",
79114
working_directory = %dir.display(),
80115
"Working directory set"
81116
);
82117
}
118+
}
83119

120+
/// Executes the command and captures its output.
121+
///
122+
/// Returns a tuple of (`exit_status`, `stdout`, `stderr`).
123+
fn execute_command(
124+
command: &mut Command,
125+
command_display: &str,
126+
) -> Result<(std::process::ExitStatus, String, String), CommandError> {
84127
let output = command
85128
.stdout(Stdio::piped())
86129
.stderr(Stdio::piped())
87130
.output()
88131
.map_err(|source| CommandError::StartupFailed {
89-
command: command_display.clone(),
132+
command: command_display.to_string(),
90133
source,
91134
})?;
92135

93-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
136+
let (stdout, stderr) = Self::extract_output(&output);
137+
138+
Ok((output.status, stdout, stderr))
139+
}
140+
141+
/// Extracts stdout and stderr from command output as strings.
142+
fn extract_output(output: &std::process::Output) -> (String, String) {
94143
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
144+
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
145+
(stdout, stderr)
146+
}
95147

96-
if !output.status.success() {
97-
let exit_code = output
98-
.status
148+
/// Checks if the command executed successfully and returns an error if it failed.
149+
fn check_command_success(
150+
status: std::process::ExitStatus,
151+
command_display: &str,
152+
stdout: &str,
153+
stderr: &str,
154+
) -> Result<(), CommandError> {
155+
if !status.success() {
156+
let exit_code = status
99157
.code()
100158
.map_or_else(|| "unknown".to_string(), |code| code.to_string());
101159

102160
return Err(CommandError::ExecutionFailed {
103-
command: command_display,
161+
command: command_display.to_string(),
104162
exit_code,
105-
stdout,
106-
stderr,
163+
stdout: stdout.to_string(),
164+
stderr: stderr.to_string(),
107165
});
108166
}
167+
Ok(())
168+
}
109169

110-
// Log stdout and stderr at debug level when command succeeds
170+
/// Logs the command output (stdout/stderr) at debug level.
171+
fn log_command_output(command_display: &str, stdout: &str, stderr: &str) {
111172
if !stdout.trim().is_empty() {
112173
tracing::debug!(
113174
operation = "command_execution",
@@ -125,8 +186,6 @@ impl CommandExecutor {
125186
stderr.trim()
126187
);
127188
}
128-
129-
Ok(CommandResult::new(output.status, stdout, stderr))
130189
}
131190
}
132191

0 commit comments

Comments
 (0)