1
1
use std:: path:: Path ;
2
- use zed_extension_api:: { Command , Result } ;
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
32
4
33
/// A simple wrapper around the `bundle` command.
5
34
pub struct Bundler {
6
35
pub working_dir : String ,
7
36
envs : Vec < ( String , String ) > ,
37
+ command_executor : Box < dyn CommandExecutor > ,
8
38
}
9
39
10
40
impl Bundler {
11
- pub fn new ( working_dir : String , envs : Vec < ( String , String ) > ) -> Self {
12
- Bundler { working_dir, envs }
41
+ /// Creates a new `Bundler` instance.
42
+ ///
43
+ /// # Arguments
44
+ /// * `working_dir` - The working directory where `bundle` commands should be executed.
45
+ /// * `command_executor` - An executor for `bundle` commands.
46
+ pub fn new (
47
+ working_dir : String ,
48
+ envs : Vec < ( String , String ) > ,
49
+ command_executor : Box < dyn CommandExecutor > ,
50
+ ) -> Self {
51
+ Bundler {
52
+ working_dir,
53
+ envs,
54
+ command_executor,
55
+ }
13
56
}
14
57
58
+ /// Retrieves the installed version of a gem using `bundle info --version <name>`.
59
+ ///
60
+ /// # Arguments
61
+ /// * `name` - The name of the gem.
62
+ ///
63
+ /// # Returns
64
+ /// A `Result` containing the version string if successful, or an error message.
15
65
pub fn installed_gem_version ( & self , name : & str ) -> Result < String > {
16
66
let args = vec ! [ "--version" . into( ) , name. into( ) ] ;
17
67
@@ -22,14 +72,10 @@ impl Bundler {
22
72
let bundle_gemfile_path = Path :: new ( & self . working_dir ) . join ( "Gemfile" ) ;
23
73
let bundle_gemfile = bundle_gemfile_path
24
74
. to_str ( )
25
- . ok_or ( "Invalid path to Gemfile" ) ?;
75
+ . ok_or_else ( || "Invalid path to Gemfile" . to_string ( ) ) ?;
26
76
27
- Command :: new ( "bundle" )
28
- . arg ( cmd)
29
- . args ( args)
30
- . envs ( self . envs . clone ( ) )
31
- . env ( "BUNDLE_GEMFILE" , bundle_gemfile)
32
- . output ( )
77
+ self . command_executor
78
+ . execute_bundle ( cmd, args, self . envs . clone ( ) , bundle_gemfile)
33
79
. and_then ( |output| match output. status {
34
80
Some ( 0 ) => Ok ( String :: from_utf8_lossy ( & output. stdout ) . to_string ( ) ) ,
35
81
Some ( status) => {
@@ -46,3 +92,178 @@ impl Bundler {
46
92
} )
47
93
}
48
94
}
95
+
96
+ #[ cfg( test) ]
97
+ mod tests {
98
+ use super :: * ;
99
+ use std:: cell:: RefCell ;
100
+
101
+ struct MockExecutorConfig {
102
+ output_to_return : Option < Result < Output > > ,
103
+ expected_sub_command : Option < String > ,
104
+ expected_args : Option < Vec < String > > ,
105
+ expected_envs : Option < Vec < ( String , String ) > > ,
106
+ expected_bundle_gemfile_path : Option < String > ,
107
+ }
108
+
109
+ struct MockCommandExecutor {
110
+ config : RefCell < MockExecutorConfig > ,
111
+ }
112
+
113
+ impl MockCommandExecutor {
114
+ fn new ( ) -> Self {
115
+ MockCommandExecutor {
116
+ config : RefCell :: new ( MockExecutorConfig {
117
+ output_to_return : None ,
118
+ expected_sub_command : None ,
119
+ expected_args : None ,
120
+ expected_envs : None ,
121
+ expected_bundle_gemfile_path : None ,
122
+ } ) ,
123
+ }
124
+ }
125
+
126
+ fn expect (
127
+ & self ,
128
+ sub_command : & str ,
129
+ args : & [ & str ] ,
130
+ envs : & [ ( & str , & str ) ] ,
131
+ bundle_gemfile_path : & str ,
132
+ output : super :: Result < Output > ,
133
+ ) {
134
+ 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 ( ) ) ;
137
+ config. expected_envs = Some (
138
+ envs. iter ( )
139
+ . map ( |& ( k, v) | ( k. to_string ( ) , v. to_string ( ) ) )
140
+ . collect ( ) ,
141
+ ) ;
142
+ config. expected_bundle_gemfile_path = Some ( bundle_gemfile_path. to_string ( ) ) ;
143
+ config. output_to_return = Some ( output) ;
144
+ }
145
+ }
146
+
147
+ impl CommandExecutor for MockCommandExecutor {
148
+ fn execute_bundle (
149
+ & self ,
150
+ sub_command : String ,
151
+ args : Vec < String > ,
152
+ envs : Vec < ( String , String ) > ,
153
+ bundle_gemfile_path : & str ,
154
+ ) -> super :: Result < Output > {
155
+ let mut config = self . config . borrow_mut ( ) ;
156
+
157
+ if let Some ( expected_cmd) = & config. expected_sub_command {
158
+ assert_eq ! ( & sub_command, expected_cmd, "Mock: Sub-command mismatch" ) ;
159
+ }
160
+ if let Some ( expected_args) = & config. expected_args {
161
+ assert_eq ! ( & args, expected_args, "Mock: Args mismatch" ) ;
162
+ }
163
+ if let Some ( expected_envs) = & config. expected_envs {
164
+ assert_eq ! ( & envs, expected_envs, "Mock: Env mismatch" ) ;
165
+ }
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
+ }
172
+
173
+ config. output_to_return . take ( ) . expect (
174
+ "MockCommandExecutor: output_to_return was not set or already consumed for the test" ,
175
+ )
176
+ }
177
+ }
178
+
179
+ fn create_mock_executor_for_success (
180
+ version : & str ,
181
+ dir : & str ,
182
+ gem : & str ,
183
+ ) -> MockCommandExecutor {
184
+ let mock = MockCommandExecutor :: new ( ) ;
185
+ mock. expect (
186
+ "info" ,
187
+ & [ "--version" , gem] ,
188
+ & [ ] ,
189
+ & format ! ( "{}/Gemfile" , dir) ,
190
+ Ok ( Output {
191
+ status : Some ( 0 ) ,
192
+ stdout : version. as_bytes ( ) . to_vec ( ) ,
193
+ stderr : Vec :: new ( ) ,
194
+ } ) ,
195
+ ) ;
196
+ mock
197
+ }
198
+
199
+ #[ test]
200
+ fn test_installed_gem_version_success ( ) {
201
+ let mock_executor = create_mock_executor_for_success ( "8.0.0" , "test_dir" , "rails" ) ;
202
+ let bundler = Bundler :: new ( "test_dir" . into ( ) , vec ! [ ] , Box :: new ( mock_executor) ) ;
203
+ let version = bundler
204
+ . installed_gem_version ( "rails" )
205
+ . expect ( "Expected successful version" ) ;
206
+ assert_eq ! ( version, "8.0.0" , "Installed gem version should match" ) ;
207
+ }
208
+
209
+ #[ test]
210
+ fn test_installed_gem_version_command_error ( ) {
211
+ let mock_executor = MockCommandExecutor :: new ( ) ;
212
+ let gem_name = "unknown_gem" ;
213
+ let error_output = "Could not find gem 'unknown_gem'." ;
214
+
215
+ mock_executor. expect (
216
+ "info" ,
217
+ & [ "--version" , gem_name] ,
218
+ & [ ] ,
219
+ "test_dir/Gemfile" ,
220
+ Ok ( Output {
221
+ status : Some ( 1 ) ,
222
+ stdout : Vec :: new ( ) ,
223
+ stderr : error_output. as_bytes ( ) . to_vec ( ) ,
224
+ } ) ,
225
+ ) ;
226
+
227
+ let bundler = Bundler :: new ( "test_dir" . into ( ) , vec ! [ ] , Box :: new ( mock_executor) ) ;
228
+ let result = bundler. installed_gem_version ( gem_name) ;
229
+
230
+ assert ! (
231
+ result. is_err( ) ,
232
+ "Expected error for failed gem version check"
233
+ ) ;
234
+ let err_msg = result. unwrap_err ( ) ;
235
+ assert ! (
236
+ err_msg. contains( "'bundle' command failed (status: 1)" ) ,
237
+ "Error message should contain status"
238
+ ) ;
239
+ assert ! (
240
+ err_msg. contains( error_output) ,
241
+ "Error message should contain stderr output"
242
+ ) ;
243
+ }
244
+
245
+ #[ test]
246
+ fn test_installed_gem_version_execution_failure_from_executor ( ) {
247
+ let mock_executor = MockCommandExecutor :: new ( ) ;
248
+ let gem_name = "critical_gem" ;
249
+ let specific_error_msg = "Mocked execution failure" ;
250
+
251
+ mock_executor. expect (
252
+ "info" ,
253
+ & [ "--version" , gem_name] ,
254
+ & [ ] ,
255
+ "test_dir/Gemfile" ,
256
+ Err ( specific_error_msg. to_string ( ) ) ,
257
+ ) ;
258
+
259
+ let bundler = Bundler :: new ( "test_dir" . into ( ) , vec ! [ ] , Box :: new ( mock_executor) ) ;
260
+ let result = bundler. installed_gem_version ( gem_name) ;
261
+
262
+ assert ! ( result. is_err( ) , "Expected error from executor failure" ) ;
263
+ assert_eq ! (
264
+ result. unwrap_err( ) ,
265
+ specific_error_msg,
266
+ "Error message should match executor error"
267
+ ) ;
268
+ }
269
+ }
0 commit comments