11use crate :: command_executor:: CommandExecutor ;
22use regex:: Regex ;
33use std:: {
4- path:: PathBuf ,
4+ collections:: hash_map:: DefaultHasher ,
5+ hash:: { Hash , Hasher } ,
6+ path:: { Path , PathBuf } ,
57 sync:: { LazyLock , OnceLock } ,
68} ;
79
10+ pub fn versioned_gem_home (
11+ base_dir : & Path ,
12+ envs : & [ ( & str , & str ) ] ,
13+ executor : & dyn CommandExecutor ,
14+ ) -> Result < PathBuf , String > {
15+ let output = executor
16+ . execute ( "ruby" , & [ "--version" ] , envs)
17+ . map_err ( |e| format ! ( "Failed to detect Ruby version: {e}" ) ) ?;
18+
19+ match output. status {
20+ Some ( 0 ) => {
21+ let version_string = String :: from_utf8_lossy ( & output. stdout ) ;
22+ let mut hasher = DefaultHasher :: new ( ) ;
23+ version_string. trim ( ) . hash ( & mut hasher) ;
24+ let version_hash = format ! ( "{:x}" , hasher. finish( ) ) ;
25+ Ok ( base_dir. join ( "gems" ) . join ( version_hash) )
26+ }
27+ Some ( status) => Err ( format ! ( "Ruby version check failed with status {status}" ) ) ,
28+ None => Err ( "Failed to execute ruby --version" . to_string ( ) ) ,
29+ }
30+ }
31+
832/// A simple wrapper around the `gem` command.
933pub struct Gemset {
1034 gem_home : PathBuf ,
@@ -176,6 +200,7 @@ mod tests {
176200 use super :: * ;
177201 use crate :: command_executor:: CommandExecutor ;
178202 use std:: cell:: RefCell ;
203+ use std:: path:: Path ;
179204 use zed_extension_api:: process:: Output ;
180205
181206 struct MockExecutorConfig {
@@ -185,13 +210,13 @@ mod tests {
185210 output_to_return : Option < Result < Output , String > > ,
186211 }
187212
188- struct MockGemCommandExecutor {
213+ struct MockCommandExecutor {
189214 config : RefCell < MockExecutorConfig > ,
190215 }
191216
192- impl MockGemCommandExecutor {
217+ impl MockCommandExecutor {
193218 fn new ( ) -> Self {
194- MockGemCommandExecutor {
219+ MockCommandExecutor {
195220 config : RefCell :: new ( MockExecutorConfig {
196221 expected_command_name : None ,
197222 expected_args : None ,
@@ -221,7 +246,7 @@ mod tests {
221246 }
222247 }
223248
224- impl CommandExecutor for MockGemCommandExecutor {
249+ impl CommandExecutor for MockCommandExecutor {
225250 fn execute (
226251 & self ,
227252 command_name : & str ,
@@ -247,26 +272,158 @@ mod tests {
247272 config
248273 . output_to_return
249274 . take ( )
250- . expect ( "MockGemCommandExecutor : output_to_return was not set or already consumed" )
275+ . expect ( "MockCommandExecutor : output_to_return was not set or already consumed" )
251276 }
252277 }
253278
254279 const TEST_GEM_HOME : & str = "/test/gem_home" ;
255280 const TEST_GEM_PATH : & str = "/test/gem_path" ;
256281
257- fn create_gemset (
258- envs : Option < & [ ( & str , & str ) ] > ,
259- mock_executor : MockGemCommandExecutor ,
260- ) -> Gemset {
282+ fn create_gemset ( envs : Option < & [ ( & str , & str ) ] > , mock_executor : MockCommandExecutor ) -> Gemset {
261283 Gemset :: new ( TEST_GEM_HOME . into ( ) , envs, Box :: new ( mock_executor) )
262284 }
263285
286+ #[ test]
287+ fn test_versioned_gem_home_success ( ) {
288+ let executor = MockCommandExecutor :: new ( ) ;
289+ executor. expect (
290+ "ruby" ,
291+ & [ "--version" ] ,
292+ & [ ] ,
293+ Ok ( Output {
294+ status : Some ( 0 ) ,
295+ stdout : "ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]\n "
296+ . as_bytes ( )
297+ . to_vec ( ) ,
298+ stderr : Vec :: new ( ) ,
299+ } ) ,
300+ ) ;
301+
302+ let result = versioned_gem_home ( Path :: new ( "/extension" ) , & [ ] , & executor) ;
303+ assert ! ( result. is_ok( ) ) ;
304+ let path = result. expect ( "should return path" ) ;
305+ assert ! ( path. starts_with( "/extension/gems/" ) ) ;
306+ assert_eq ! ( path. components( ) . count( ) , 4 ) ;
307+ }
308+
309+ #[ test]
310+ fn test_versioned_gem_home_different_versions_produce_different_hashes ( ) {
311+ let executor1 = MockCommandExecutor :: new ( ) ;
312+ executor1. expect (
313+ "ruby" ,
314+ & [ "--version" ] ,
315+ & [ ] ,
316+ Ok ( Output {
317+ status : Some ( 0 ) ,
318+ stdout : "ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]\n "
319+ . as_bytes ( )
320+ . to_vec ( ) ,
321+ stderr : Vec :: new ( ) ,
322+ } ) ,
323+ ) ;
324+
325+ let executor2 = MockCommandExecutor :: new ( ) ;
326+ executor2. expect (
327+ "ruby" ,
328+ & [ "--version" ] ,
329+ & [ ] ,
330+ Ok ( Output {
331+ status : Some ( 0 ) ,
332+ stdout : "ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin23]\n "
333+ . as_bytes ( )
334+ . to_vec ( ) ,
335+ stderr : Vec :: new ( ) ,
336+ } ) ,
337+ ) ;
338+
339+ let path1 = versioned_gem_home ( Path :: new ( "/extension" ) , & [ ] , & executor1)
340+ . expect ( "should return path" ) ;
341+ let path2 = versioned_gem_home ( Path :: new ( "/extension" ) , & [ ] , & executor2)
342+ . expect ( "should return path" ) ;
343+
344+ assert_ne ! ( path1, path2) ;
345+ }
346+
347+ #[ test]
348+ fn test_versioned_gem_home_same_version_produces_same_hash ( ) {
349+ let version_output = "ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]\n " ;
350+
351+ let executor1 = MockCommandExecutor :: new ( ) ;
352+ executor1. expect (
353+ "ruby" ,
354+ & [ "--version" ] ,
355+ & [ ] ,
356+ Ok ( Output {
357+ status : Some ( 0 ) ,
358+ stdout : version_output. as_bytes ( ) . to_vec ( ) ,
359+ stderr : Vec :: new ( ) ,
360+ } ) ,
361+ ) ;
362+
363+ let executor2 = MockCommandExecutor :: new ( ) ;
364+ executor2. expect (
365+ "ruby" ,
366+ & [ "--version" ] ,
367+ & [ ] ,
368+ Ok ( Output {
369+ status : Some ( 0 ) ,
370+ stdout : version_output. as_bytes ( ) . to_vec ( ) ,
371+ stderr : Vec :: new ( ) ,
372+ } ) ,
373+ ) ;
374+
375+ let path1 = versioned_gem_home ( Path :: new ( "/extension" ) , & [ ] , & executor1)
376+ . expect ( "should return path" ) ;
377+ let path2 = versioned_gem_home ( Path :: new ( "/extension" ) , & [ ] , & executor2)
378+ . expect ( "should return path" ) ;
379+
380+ assert_eq ! ( path1, path2) ;
381+ }
382+
383+ #[ test]
384+ fn test_versioned_gem_home_command_failure ( ) {
385+ let executor = MockCommandExecutor :: new ( ) ;
386+ executor. expect (
387+ "ruby" ,
388+ & [ "--version" ] ,
389+ & [ ] ,
390+ Ok ( Output {
391+ status : Some ( 127 ) ,
392+ stdout : Vec :: new ( ) ,
393+ stderr : "ruby: command not found" . as_bytes ( ) . to_vec ( ) ,
394+ } ) ,
395+ ) ;
396+
397+ let result = versioned_gem_home ( Path :: new ( "/extension" ) , & [ ] , & executor) ;
398+ assert ! ( result. is_err( ) ) ;
399+ assert ! ( result
400+ . expect_err( "should return error" )
401+ . contains( "Ruby version check failed with status 127" ) ) ;
402+ }
403+
404+ #[ test]
405+ fn test_versioned_gem_home_execution_error ( ) {
406+ let executor = MockCommandExecutor :: new ( ) ;
407+ executor. expect (
408+ "ruby" ,
409+ & [ "--version" ] ,
410+ & [ ] ,
411+ Err ( "Failed to spawn process" . to_string ( ) ) ,
412+ ) ;
413+
414+ let result = versioned_gem_home ( Path :: new ( "/extension" ) , & [ ] , & executor) ;
415+ assert ! ( result. is_err( ) ) ;
416+ assert ! ( result
417+ . expect_err( "should return error" )
418+ . contains( "Failed to detect Ruby version" ) ) ;
419+ }
420+
264421 #[ test]
265422 fn test_gem_bin_path ( ) {
266423 let gemset = Gemset :: new (
267424 TEST_GEM_HOME . into ( ) ,
268425 None ,
269- Box :: new ( MockGemCommandExecutor :: new ( ) ) ,
426+ Box :: new ( MockCommandExecutor :: new ( ) ) ,
270427 ) ;
271428 let path = gemset. gem_bin_path ( "ruby-lsp" ) . unwrap ( ) ;
272429 assert_eq ! ( path, "/test/gem_home/bin/ruby-lsp" ) ;
@@ -277,7 +434,7 @@ mod tests {
277434 let gemset = Gemset :: new (
278435 TEST_GEM_HOME . into ( ) ,
279436 Some ( & [ ( "GEM_PATH" , TEST_GEM_PATH ) , ( "PATH" , "/usr/bin" ) ] ) ,
280- Box :: new ( MockGemCommandExecutor :: new ( ) ) ,
437+ Box :: new ( MockCommandExecutor :: new ( ) ) ,
281438 ) ;
282439 let env: std:: collections:: HashMap < String , String > = gemset. env ( ) . iter ( ) . cloned ( ) . collect ( ) ;
283440
@@ -291,7 +448,7 @@ mod tests {
291448
292449 #[ test]
293450 fn test_install_gem_success ( ) {
294- let mock_executor = MockGemCommandExecutor :: new ( ) ;
451+ let mock_executor = MockCommandExecutor :: new ( ) ;
295452 let gem_name = "ruby-lsp" ;
296453 mock_executor. expect (
297454 "gem" ,
@@ -316,7 +473,7 @@ mod tests {
316473
317474 #[ test]
318475 fn test_install_gem_with_custom_env ( ) {
319- let mock_executor = MockGemCommandExecutor :: new ( ) ;
476+ let mock_executor = MockCommandExecutor :: new ( ) ;
320477 let gem_name = "ruby-lsp" ;
321478 mock_executor. expect (
322479 "gem" ,
@@ -345,7 +502,7 @@ mod tests {
345502
346503 #[ test]
347504 fn test_install_gem_failure ( ) {
348- let mock_executor = MockGemCommandExecutor :: new ( ) ;
505+ let mock_executor = MockCommandExecutor :: new ( ) ;
349506 let gem_name = "ruby-lsp" ;
350507 mock_executor. expect (
351508 "gem" ,
@@ -374,7 +531,7 @@ mod tests {
374531
375532 #[ test]
376533 fn test_update_gem_success ( ) {
377- let mock_executor = MockGemCommandExecutor :: new ( ) ;
534+ let mock_executor = MockCommandExecutor :: new ( ) ;
378535 let gem_name = "ruby-lsp" ;
379536 mock_executor. expect (
380537 "gem" ,
@@ -392,7 +549,7 @@ mod tests {
392549
393550 #[ test]
394551 fn test_update_gem_failure ( ) {
395- let mock_executor = MockGemCommandExecutor :: new ( ) ;
552+ let mock_executor = MockCommandExecutor :: new ( ) ;
396553 let gem_name = "ruby-lsp" ;
397554 mock_executor. expect (
398555 "gem" ,
@@ -414,7 +571,7 @@ mod tests {
414571
415572 #[ test]
416573 fn test_installed_gem_version_found ( ) {
417- let mock_executor = MockGemCommandExecutor :: new ( ) ;
574+ let mock_executor = MockCommandExecutor :: new ( ) ;
418575 let gem_name = "ruby-lsp" ;
419576 let expected_version = "1.2.3" ;
420577 let gem_list_output = format ! (
@@ -439,7 +596,7 @@ mod tests {
439596
440597 #[ test]
441598 fn test_installed_gem_version_found_with_default ( ) {
442- let mock_executor = MockGemCommandExecutor :: new ( ) ;
599+ let mock_executor = MockCommandExecutor :: new ( ) ;
443600 let gem_name = "prism" ;
444601 let version_in_output = "default: 1.2.0" ;
445602 let gem_list_output = format ! (
@@ -464,7 +621,7 @@ mod tests {
464621
465622 #[ test]
466623 fn test_installed_gem_version_not_found ( ) {
467- let mock_executor = MockGemCommandExecutor :: new ( ) ;
624+ let mock_executor = MockCommandExecutor :: new ( ) ;
468625 let gem_name = "non_existent_gem" ;
469626 let gem_list_output = "other_gem (1.0.0)\n another_gem (2.0.0)" ;
470627
@@ -485,7 +642,7 @@ mod tests {
485642
486643 #[ test]
487644 fn test_installed_gem_version_command_failure ( ) {
488- let mock_executor = MockGemCommandExecutor :: new ( ) ;
645+ let mock_executor = MockCommandExecutor :: new ( ) ;
489646 let gem_name = "ruby-lsp" ;
490647 mock_executor. expect (
491648 "gem" ,
@@ -507,7 +664,7 @@ mod tests {
507664
508665 #[ test]
509666 fn test_is_outdated_gem_true ( ) {
510- let mock_executor = MockGemCommandExecutor :: new ( ) ;
667+ let mock_executor = MockCommandExecutor :: new ( ) ;
511668 let gem_name = "ruby-lsp" ;
512669 let outdated_output = format ! (
513670 "{} (3.3.2 < 3.3.4)\n {} (2.9.1 < 2.11.3)\n {} (0.5.6 < 0.5.8)" ,
@@ -531,7 +688,7 @@ mod tests {
531688
532689 #[ test]
533690 fn test_is_outdated_gem_false ( ) {
534- let mock_executor = MockGemCommandExecutor :: new ( ) ;
691+ let mock_executor = MockCommandExecutor :: new ( ) ;
535692 let gem_name = "ruby-lsp" ;
536693 let outdated_output = "csv (3.3.2 < 3.3.4)" ;
537694
@@ -552,7 +709,7 @@ mod tests {
552709
553710 #[ test]
554711 fn test_is_outdated_gem_command_failure ( ) {
555- let mock_executor = MockGemCommandExecutor :: new ( ) ;
712+ let mock_executor = MockCommandExecutor :: new ( ) ;
556713 let gem_name = "ruby-lsp" ;
557714 mock_executor. expect (
558715 "gem" ,
@@ -574,7 +731,7 @@ mod tests {
574731
575732 #[ test]
576733 fn test_uninstall_gem_success ( ) {
577- let mock_executor = MockGemCommandExecutor :: new ( ) ;
734+ let mock_executor = MockCommandExecutor :: new ( ) ;
578735 let gem_name = "solargraph" ;
579736 let gem_version = "0.55.1" ;
580737
@@ -596,7 +753,7 @@ mod tests {
596753
597754 #[ test]
598755 fn test_uninstall_gem_failure ( ) {
599- let mock_executor = MockGemCommandExecutor :: new ( ) ;
756+ let mock_executor = MockCommandExecutor :: new ( ) ;
600757 let gem_name = "solargraph" ;
601758 let gem_version = "0.55.1" ;
602759
@@ -622,7 +779,7 @@ mod tests {
622779
623780 #[ test]
624781 fn test_uninstall_gem_command_execution_error ( ) {
625- let mock_executor = MockGemCommandExecutor :: new ( ) ;
782+ let mock_executor = MockCommandExecutor :: new ( ) ;
626783 let gem_name = "solargraph" ;
627784 let gem_version = "0.55.1" ;
628785
0 commit comments