ukiryu 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ukiryu/cache.rb +6 -0
  3. data/lib/ukiryu/cache_registry.rb +64 -0
  4. data/lib/ukiryu/cli_commands/base_command.rb +6 -5
  5. data/lib/ukiryu/cli_commands/config_command.rb +7 -10
  6. data/lib/ukiryu/cli_commands/register_command.rb +27 -18
  7. data/lib/ukiryu/cli_commands/validate_command.rb +2 -2
  8. data/lib/ukiryu/command_builder.rb +83 -50
  9. data/lib/ukiryu/config.rb +13 -2
  10. data/lib/ukiryu/debug.rb +20 -9
  11. data/lib/ukiryu/definition/loader.rb +3 -3
  12. data/lib/ukiryu/errors.rb +37 -37
  13. data/lib/ukiryu/executable_locator.rb +40 -16
  14. data/lib/ukiryu/extractors/base_extractor.rb +2 -1
  15. data/lib/ukiryu/extractors/help_parser.rb +3 -0
  16. data/lib/ukiryu/logger.rb +51 -0
  17. data/lib/ukiryu/models/implementation_index.rb +2 -1
  18. data/lib/ukiryu/models/implementation_version.rb +18 -1
  19. data/lib/ukiryu/models/interface.rb +2 -1
  20. data/lib/ukiryu/models/run_environment.rb +0 -2
  21. data/lib/ukiryu/models/semantic_version.rb +174 -0
  22. data/lib/ukiryu/models/stage_metrics.rb +0 -1
  23. data/lib/ukiryu/register.rb +473 -232
  24. data/lib/ukiryu/shell/powershell.rb +192 -99
  25. data/lib/ukiryu/shell/sh.rb +4 -1
  26. data/lib/ukiryu/shell.rb +60 -2
  27. data/lib/ukiryu/tool/command_resolution.rb +2 -1
  28. data/lib/ukiryu/tool/executable_discovery.rb +14 -15
  29. data/lib/ukiryu/tool/loader.rb +543 -0
  30. data/lib/ukiryu/tool/version_detection.rb +1 -3
  31. data/lib/ukiryu/tool.rb +79 -87
  32. data/lib/ukiryu/tool_index.rb +127 -62
  33. data/lib/ukiryu/tools/base.rb +4 -2
  34. data/lib/ukiryu/type.rb +26 -15
  35. data/lib/ukiryu/version.rb +1 -1
  36. data/lib/ukiryu.rb +1 -1
  37. data/spec/fixtures/profiles/ghostscript_10.0.yaml +50 -0
  38. data/spec/fixtures/register/tools/ghostscript/default/10.0.yaml +6 -0
  39. data/spec/fixtures/register/tools/inkscape/INKSCAPE_MAN.md +444 -0
  40. data/spec/spec_helper.rb +10 -6
  41. data/spec/support/tool_helper.rb +2 -0
  42. data/spec/ukiryu/definition/loader_spec.rb +2 -2
  43. data/spec/ukiryu/executor_spec.rb +6 -3
  44. data/spec/ukiryu/models/execution_report_spec.rb +3 -2
  45. data/spec/ukiryu/models/semantic_version_spec.rb +284 -0
  46. data/spec/ukiryu/shell/powershell_integration_spec.rb +165 -0
  47. data/spec/ukiryu/shell/powershell_real_command_spec.rb +143 -0
  48. data/spec/ukiryu/shell/powershell_spec.rb +228 -60
  49. data/spec/ukiryu/tool/loader_spec.rb +148 -0
  50. data/spec/ukiryu/tool_index_spec.rb +110 -18
  51. data/spec/ukiryu/tools/ghostscript_spec.rb +242 -0
  52. data/spec/ukiryu/tools/imagemagick_spec.rb +2 -1
  53. data/spec/ukiryu/tools/inkscape_spec.rb +4 -2
  54. metadata +21 -4
  55. data/lib/ukiryu/register_auto_manager.rb +0 -342
data/spec/spec_helper.rb CHANGED
@@ -8,7 +8,7 @@ require 'timeout'
8
8
  require 'ukiryu'
9
9
 
10
10
  # Require all support files
11
- Dir[File.join(__dir__, "support", "**", "*.rb")].each { |f| require f }
11
+ Dir[File.join(__dir__, 'support', '**', '*.rb')].sort.each { |f| require f }
12
12
 
13
13
  RSpec.configure do |config|
14
14
  # Enable flags like --only-failures and --next-failure
@@ -40,12 +40,12 @@ RSpec.configure do |config|
40
40
  # Set up test register path if UKIRYU_REGISTER is not already set
41
41
  unless ENV['UKIRYU_REGISTER']
42
42
  test_register = File.expand_path('fixtures/register', __dir__)
43
- if Dir.exist?(test_register)
44
- ENV['UKIRYU_REGISTER'] = test_register
45
- Ukiryu::Register.default_register_path = test_register
46
- end
43
+ ENV['UKIRYU_REGISTER'] = test_register if Dir.exist?(test_register)
47
44
  end
48
45
 
46
+ # Reset the default register to pick up the test register
47
+ Ukiryu::Register.reset_default
48
+
49
49
  # Reset ToolIndex to pick up the new register path
50
50
  Ukiryu::ToolIndex.reset
51
51
 
@@ -59,11 +59,15 @@ RSpec.configure do |config|
59
59
  # Reset singleton state before each test to prevent pollution
60
60
  config.before(:each) do
61
61
  Ukiryu::Config.reset!
62
- Ukiryu::Register.reset_version_cache
62
+ Ukiryu::Register.reset_default
63
63
  Ukiryu::ToolIndex.reset
64
64
  Ukiryu::Tool.clear_cache
65
65
  Ukiryu::Runtime.instance.reset!
66
66
  Ukiryu::Tools::Generator.clear_cache
67
+ Ukiryu::Shell.reset
68
+
69
+ # Clean up test shell environment variable that may be left by other tests
70
+ ENV.delete('UKIRYU_TEST_SHELL')
67
71
 
68
72
  # Remove generated tool classes from Tools namespace
69
73
  Ukiryu::Tools.constants.each do |const|
@@ -15,6 +15,8 @@ module ToolHelper
15
15
  def tool_available?(name)
16
16
  tool = get_tool(name)
17
17
  tool.available?
18
+ rescue Ukiryu::Errors::ToolNotFoundError
19
+ false
18
20
  end
19
21
 
20
22
  # Get tool version if available
@@ -174,8 +174,8 @@ RSpec.describe Ukiryu::Definition::Loader do
174
174
  end
175
175
 
176
176
  describe '.profile_cache' do
177
- it 'returns a hash' do
178
- expect(described_class.profile_cache).to be_a(Hash)
177
+ it 'returns a bounded cache' do
178
+ expect(described_class.profile_cache).to be_a(Ukiryu::Cache)
179
179
  end
180
180
 
181
181
  it 'returns same cache on multiple calls' do
@@ -152,7 +152,8 @@ RSpec.describe Ukiryu::Executor do
152
152
  # Use platform-appropriate command that exits with specific code
153
153
  result = if Ukiryu::Platform.windows?
154
154
  # Windows: PowerShell supports exit codes
155
- executor.execute('powershell', ['-Command', 'exit 42'], allow_failure: true, shell: :powershell, timeout: 30)
155
+ executor.execute('powershell', ['-Command', 'exit 42'], allow_failure: true, shell: :powershell,
156
+ timeout: 30)
156
157
  else
157
158
  executor.execute('sh', ['-c', 'exit 42'], allow_failure: true, shell: shell_symbol, timeout: 30)
158
159
  end
@@ -215,7 +216,8 @@ RSpec.describe Ukiryu::Executor do
215
216
  # Use echo as a cross-platform command that always works
216
217
  result = if Ukiryu::Platform.windows?
217
218
  # On Windows with PowerShell, use echo command via PowerShell
218
- executor.execute('powershell', ['-Command', 'echo test'], allow_failure: true, shell: :powershell, timeout: 90)
219
+ executor.execute('powershell', ['-Command', 'echo test'], allow_failure: true, shell: :powershell,
220
+ timeout: 90)
219
221
  else
220
222
  executor.execute('echo', ['test'], allow_failure: true, shell: shell_symbol, timeout: 90)
221
223
  end
@@ -415,7 +417,8 @@ RSpec.describe Ukiryu::Executor do
415
417
  end
416
418
 
417
419
  it 'handles commands with special characters in arguments' do
418
- result = executor.execute('echo', ['hello "quoted" test\'s'], allow_failure: true, shell: shell_symbol, timeout: 90)
420
+ result = executor.execute('echo', ['hello "quoted" test\'s'], allow_failure: true, shell: shell_symbol,
421
+ timeout: 90)
419
422
 
420
423
  expect(result.stdout.strip).to include('hello')
421
424
  end
@@ -77,9 +77,10 @@ RSpec.describe Ukiryu::Models::ExecutionReport do
77
77
  # In Docker/CI, may be 0 or an actual value
78
78
  expect(stage.memory_after).to be_a(Integer)
79
79
  expect(stage.memory_after).to be >= 0
80
- # If memory detection works, memory_after should be >= memory_before
81
- expect(stage.memory_after).to be >= stage.memory_before if stage.memory_before > 0
80
+ # Memory delta is calculated (may be negative due to GC)
82
81
  expect(stage.memory_delta).to be_a(Integer)
82
+ # Verify delta calculation is correct
83
+ expect(stage.memory_delta).to eq(stage.memory_after - stage.memory_before)
83
84
  end
84
85
 
85
86
  it 'marks the stage as successful by default' do
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'ukiryu/models/semantic_version'
5
+
6
+ RSpec.describe Ukiryu::Models::SemanticVersion do
7
+ describe '.parse' do
8
+ it 'parses simple version' do
9
+ expect(described_class.parse('10.0')).to eq([10, 0])
10
+ end
11
+
12
+ it 'parses three-part version' do
13
+ expect(described_class.parse('1.2.3')).to eq([1, 2, 3])
14
+ end
15
+
16
+ it 'parses single number version' do
17
+ expect(described_class.parse('5')).to eq([5])
18
+ end
19
+
20
+ it 'handles nil' do
21
+ expect(described_class.parse(nil)).to eq([0])
22
+ end
23
+
24
+ it 'handles empty string' do
25
+ expect(described_class.parse('')).to eq([0])
26
+ end
27
+
28
+ it 'handles non-numeric parts as 0' do
29
+ expect(described_class.parse('1.alpha.3')).to eq([1, 0, 3])
30
+ end
31
+ end
32
+
33
+ describe '.compare' do
34
+ it 'returns 1 when first version is greater' do
35
+ expect(described_class.compare('10.0', '9.5')).to eq(1)
36
+ end
37
+
38
+ it 'returns -1 when first version is lesser' do
39
+ expect(described_class.compare('9.5', '10.0')).to eq(-1)
40
+ end
41
+
42
+ it 'returns 0 when versions are equal' do
43
+ expect(described_class.compare('10.0', '10.0')).to eq(0)
44
+ end
45
+
46
+ it 'handles different segment counts' do
47
+ expect(described_class.compare('10.0.1', '10.0')).to eq(1)
48
+ end
49
+
50
+ it 'handles single vs multi-part versions' do
51
+ expect(described_class.compare('10', '9.5')).to eq(1)
52
+ end
53
+ end
54
+
55
+ describe '#initialize' do
56
+ it 'stores original string' do
57
+ version = described_class.new('10.0')
58
+ expect(version.original).to eq('10.0')
59
+ end
60
+
61
+ it 'parses segments' do
62
+ version = described_class.new('10.0.5')
63
+ expect(version.segments).to eq([10, 0, 5])
64
+ end
65
+
66
+ it 'handles integer input' do
67
+ version = described_class.new(10)
68
+ expect(version.segments).to eq([10])
69
+ end
70
+
71
+ it 'handles nil input' do
72
+ version = described_class.new(nil)
73
+ expect(version.segments).to eq([0])
74
+ end
75
+ end
76
+
77
+ describe '#<=>' do
78
+ it 'compares 10.0 > 9.5 (the critical bug case)' do
79
+ v1 = described_class.new('10.0')
80
+ v2 = described_class.new('9.5')
81
+ expect(v1 <=> v2).to eq(1)
82
+ end
83
+
84
+ it 'compares 9.5 < 10.0' do
85
+ v1 = described_class.new('9.5')
86
+ v2 = described_class.new('10.0')
87
+ expect(v1 <=> v2).to eq(-1)
88
+ end
89
+
90
+ it 'compares equal versions as 0' do
91
+ v1 = described_class.new('10.0')
92
+ v2 = described_class.new('10.0')
93
+ expect(v1 <=> v2).to eq(0)
94
+ end
95
+
96
+ it 'compares 1.10.0 > 1.9.9' do
97
+ v1 = described_class.new('1.10.0')
98
+ v2 = described_class.new('1.9.9')
99
+ expect(v1 <=> v2).to eq(1)
100
+ end
101
+
102
+ it 'compares with string' do
103
+ version = described_class.new('10.0')
104
+ expect(version <=> '9.5').to eq(1)
105
+ end
106
+
107
+ it 'compares with integer' do
108
+ version = described_class.new('10.0')
109
+ expect(version <=> 9).to eq(1)
110
+ end
111
+
112
+ it 'returns nil for non-comparable' do
113
+ version = described_class.new('10.0')
114
+ expect(version <=> Object.new).to be_nil
115
+ end
116
+ end
117
+
118
+ describe '#==' do
119
+ it 'returns true for equal versions' do
120
+ v1 = described_class.new('10.0')
121
+ v2 = described_class.new('10.0')
122
+ expect(v1 == v2).to be true
123
+ end
124
+
125
+ it 'returns true for equivalent versions with different segment counts' do
126
+ v1 = described_class.new('10.0')
127
+ v2 = described_class.new('10.0.0')
128
+ expect(v1 == v2).to be true
129
+ end
130
+
131
+ it 'returns false for different versions' do
132
+ v1 = described_class.new('10.0')
133
+ v2 = described_class.new('9.5')
134
+ expect(v1 == v2).to be false
135
+ end
136
+
137
+ it 'compares with string' do
138
+ version = described_class.new('10.0')
139
+ expect(version == '10.0').to be true
140
+ end
141
+ end
142
+
143
+ describe '#>' do
144
+ it 'returns true when greater' do
145
+ expect(described_class.new('10.0')).to be > described_class.new('9.5')
146
+ end
147
+
148
+ it 'returns false when equal' do
149
+ expect(described_class.new('10.0')).not_to be > described_class.new('10.0')
150
+ end
151
+
152
+ it 'returns false when lesser' do
153
+ expect(described_class.new('9.5')).not_to be > described_class.new('10.0')
154
+ end
155
+ end
156
+
157
+ describe '#<' do
158
+ it 'returns true when lesser' do
159
+ expect(described_class.new('9.5')).to be < described_class.new('10.0')
160
+ end
161
+
162
+ it 'returns false when equal' do
163
+ expect(described_class.new('10.0')).not_to be < described_class.new('10.0')
164
+ end
165
+ end
166
+
167
+ describe '#>=' do
168
+ it 'returns true when greater' do
169
+ expect(described_class.new('10.0')).to be >= described_class.new('9.5')
170
+ end
171
+
172
+ it 'returns true when equal' do
173
+ expect(described_class.new('10.0')).to be >= described_class.new('10.0')
174
+ end
175
+
176
+ it 'returns false when lesser' do
177
+ expect(described_class.new('9.5')).not_to be >= described_class.new('10.0')
178
+ end
179
+ end
180
+
181
+ describe '#<=' do
182
+ it 'returns true when lesser' do
183
+ expect(described_class.new('9.5')).to be <= described_class.new('10.0')
184
+ end
185
+
186
+ it 'returns true when equal' do
187
+ expect(described_class.new('10.0')).to be <= described_class.new('10.0')
188
+ end
189
+ end
190
+
191
+ describe '#to_s' do
192
+ it 'returns version string' do
193
+ version = described_class.new('10.0.5')
194
+ expect(version.to_s).to eq('10.0.5')
195
+ end
196
+ end
197
+
198
+ describe '#inspect' do
199
+ it 'returns inspect string' do
200
+ version = described_class.new('10.0')
201
+ expect(version.inspect).to eq('#<Ukiryu::Models::SemanticVersion 10.0>')
202
+ end
203
+ end
204
+
205
+ describe '#hash and #eql?' do
206
+ it 'can be used as hash key' do
207
+ v1 = described_class.new('10.0')
208
+ v2 = described_class.new('10.0')
209
+ v3 = described_class.new('9.5')
210
+
211
+ hash = { v1 => 'first', v3 => 'second' }
212
+
213
+ expect(hash[v2]).to eq('first')
214
+ expect(hash[v3]).to eq('second')
215
+ end
216
+
217
+ it 'eql? returns true for equal versions' do
218
+ v1 = described_class.new('10.0')
219
+ v2 = described_class.new('10.0')
220
+ expect(v1.eql?(v2)).to be true
221
+ end
222
+
223
+ it 'eql? returns false for different versions' do
224
+ v1 = described_class.new('10.0')
225
+ v2 = described_class.new('9.5')
226
+ expect(v1.eql?(v2)).to be false
227
+ end
228
+ end
229
+
230
+ describe 'Comparable integration' do
231
+ it 'works with Array#max' do
232
+ versions = [
233
+ described_class.new('9.5'),
234
+ described_class.new('10.0'),
235
+ described_class.new('8.0')
236
+ ]
237
+ expect(versions.max.to_s).to eq('10.0')
238
+ end
239
+
240
+ it 'works with Array#sort' do
241
+ versions = [
242
+ described_class.new('9.5'),
243
+ described_class.new('10.0'),
244
+ described_class.new('8.0')
245
+ ]
246
+ expect(versions.sort.map(&:to_s)).to eq(['8.0', '9.5', '10.0'])
247
+ end
248
+
249
+ it 'works with Array#min' do
250
+ versions = [
251
+ described_class.new('9.5'),
252
+ described_class.new('10.0'),
253
+ described_class.new('8.0')
254
+ ]
255
+ expect(versions.min.to_s).to eq('8.0')
256
+ end
257
+ end
258
+
259
+ describe 'real-world Ghostscript case' do
260
+ it 'correctly selects 10.0 over 9.5' do
261
+ v10 = described_class.new('10.0')
262
+ v95 = described_class.new('9.5')
263
+
264
+ # This was the bug: alphabetical sort would select 9.5 over 10.0
265
+ # because '9' > '1' in ASCII
266
+ expect([v95, v10].max).to eq(v10)
267
+ end
268
+
269
+ it 'demonstrates the bug with string comparison' do
270
+ # Show why string comparison is wrong
271
+ filenames = ['9.5.yaml', '10.0.yaml']
272
+
273
+ # WRONG: alphabetical max gives '9.5.yaml' (because '9' > '1')
274
+ alphabetical_max = filenames.max
275
+ expect(alphabetical_max).to eq('9.5.yaml')
276
+
277
+ # CORRECT: semantic version comparison gives '10.0.yaml'
278
+ semantic_max = filenames.max_by do |f|
279
+ described_class.new(File.basename(f, '.yaml'))
280
+ end
281
+ expect(semantic_max).to eq('10.0.yaml')
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'PowerShell Integration Tests', if: system('which pwsh > /dev/null 2>&1') do
6
+ let(:shell) { Ukiryu::Shell::PowerShell.new }
7
+ let(:env) { Ukiryu::Environment.system }
8
+
9
+ describe '#execute_command' do
10
+ context 'with dash-prefixed arguments (prefix stripping prevention)' do
11
+ it 'preserves -sDEVICE=pdfwrite style arguments' do
12
+ result = shell.execute_command('echo', ['-sDEVICE=pdfwrite', 'input.eps'], env, 30, nil)
13
+ expect(result[:status]).to eq(0)
14
+ # The full argument should be present (not stripped to just =pdfwrite)
15
+ expect(result[:stdout]).to include('-sDEVICE=pdfwrite')
16
+ end
17
+
18
+ it 'preserves multiple dash-prefixed arguments' do
19
+ args = ['-sDEVICE=pdfwrite', '-sOutputFile=output.pdf', '-dBATCH']
20
+ result = shell.execute_command('echo', args, env, 30, nil)
21
+ expect(result[:status]).to eq(0)
22
+ expect(result[:stdout]).to include('-sDEVICE=pdfwrite')
23
+ expect(result[:stdout]).to include('-sOutputFile=output.pdf')
24
+ expect(result[:stdout]).to include('-dBATCH')
25
+ end
26
+
27
+ it 'preserves -resize style arguments' do
28
+ result = shell.execute_command('echo', ['-resize', '50x50'], env, 30, nil)
29
+ expect(result[:status]).to eq(0)
30
+ expect(result[:stdout]).to include('-resize')
31
+ end
32
+ end
33
+
34
+ context 'with single quotes in arguments' do
35
+ it 'correctly escapes single quotes by doubling them' do
36
+ result = shell.execute_command('echo', ["it's a test"], env, 30, nil)
37
+ expect(result[:status]).to eq(0)
38
+ expect(result[:stdout].strip).to eq("it's a test")
39
+ end
40
+
41
+ it 'handles multiple single quotes' do
42
+ result = shell.execute_command('echo', ["it's a test's value"], env, 30, nil)
43
+ expect(result[:status]).to eq(0)
44
+ expect(result[:stdout].strip).to eq("it's a test's value")
45
+ end
46
+ end
47
+
48
+ context 'with special characters in arguments' do
49
+ it 'handles dollar signs (escaped in double quotes)' do
50
+ result = shell.execute_command('echo', ['$VAR'], env, 30, nil)
51
+ expect(result[:status]).to eq(0)
52
+ # Dollar signs are escaped with backtick in double-quoted strings
53
+ expect(result[:stdout].strip).to eq('$VAR')
54
+ end
55
+
56
+ it 'handles backticks (escaped in double quotes)' do
57
+ result = shell.execute_command('echo', ['`hello`'], env, 30, nil)
58
+ expect(result[:status]).to eq(0)
59
+ # Backticks are escaped with backtick in double-quoted strings
60
+ expect(result[:stdout].strip).to eq('`hello`')
61
+ end
62
+
63
+ it 'handles arguments with spaces' do
64
+ result = shell.execute_command('echo', ['hello world'], env, 30, nil)
65
+ expect(result[:status]).to eq(0)
66
+ expect(result[:stdout].strip).to eq('hello world')
67
+ end
68
+
69
+ it 'handles semicolons safely' do
70
+ result = shell.execute_command('echo', ['hello;world'], env, 30, nil)
71
+ expect(result[:status]).to eq(0)
72
+ expect(result[:stdout].strip).to eq('hello;world')
73
+ end
74
+ end
75
+
76
+ context 'with executable paths' do
77
+ it 'handles paths with spaces' do
78
+ # Use /bin/echo which is a simple command that exists
79
+ result = shell.execute_command('/bin/echo', ['test'], env, 30, nil)
80
+ expect(result[:status]).to eq(0)
81
+ expect(result[:stdout].strip).to eq('test')
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#join' do
87
+ context 'with dash-prefixed arguments' do
88
+ it 'quotes arguments starting with dash with double quotes' do
89
+ cmd = shell.join('gs', '-sDEVICE=pdfwrite', 'input.eps', 'output.pdf')
90
+ expect(cmd).to include('"-sDEVICE=pdfwrite"')
91
+ end
92
+
93
+ it 'quotes multiple dash-prefixed arguments' do
94
+ cmd = shell.join('gs', '-sDEVICE=pdfwrite', '-dBATCH', 'input.eps')
95
+ expect(cmd).to include('"-sDEVICE=pdfwrite"')
96
+ expect(cmd).to include('"-dBATCH"')
97
+ end
98
+ end
99
+
100
+ context 'with arguments containing special characters' do
101
+ it 'quotes and escapes arguments with dollar signs' do
102
+ cmd = shell.join('echo', '$VAR')
103
+ # Dollar sign is escaped with backtick in double-quoted strings
104
+ expect(cmd).to include('"`$VAR"')
105
+ end
106
+
107
+ it 'quotes arguments with spaces' do
108
+ cmd = shell.join('echo', 'hello world')
109
+ expect(cmd).to include('"hello world"')
110
+ end
111
+ end
112
+
113
+ context 'with simple arguments' do
114
+ it 'does not quote simple arguments' do
115
+ cmd = shell.join('echo', 'hello', 'world')
116
+ expect(cmd).to eq('echo hello world')
117
+ end
118
+ end
119
+
120
+ context 'with executable paths containing spaces' do
121
+ it 'quotes the executable path' do
122
+ cmd = shell.join('/path with space/gs', '-sDEVICE=pdfwrite')
123
+ expect(cmd).to include('"/path with space/gs"')
124
+ expect(cmd).to include('"-sDEVICE=pdfwrite"')
125
+ end
126
+ end
127
+ end
128
+
129
+ describe '#execute_command_with_stdin' do
130
+ it 'passes stdin data correctly' do
131
+ result = shell.execute_command_with_stdin('cat', [], env, 30, nil, 'hello from stdin')
132
+ expect(result[:status]).to eq(0)
133
+ expect(result[:stdout].strip).to eq('hello from stdin')
134
+ end
135
+
136
+ it 'preserves dash-prefixed arguments with stdin' do
137
+ # Use /bin/echo instead of 'echo' because PowerShell's echo alias (Write-Output)
138
+ # doesn't handle stdin piping correctly on Unix
139
+ result = shell.execute_command_with_stdin('/bin/echo', ['-sDEVICE=pdfwrite'], env, 30, nil, '')
140
+ expect(result[:status]).to eq(0)
141
+ expect(result[:stdout]).to include('-sDEVICE=pdfwrite')
142
+ end
143
+ end
144
+
145
+ describe 'real-world Ghostscript command simulation' do
146
+ it 'formats Ghostscript-style command correctly' do
147
+ # Simulate the command Vectory would run
148
+ args = [
149
+ '-sDEVICE=pdfwrite',
150
+ '-sOutputFile=output.pdf',
151
+ '-dBATCH',
152
+ '-dNOPAUSE',
153
+ 'input.ps'
154
+ ]
155
+ result = shell.execute_command('echo', args, env, 30, nil)
156
+
157
+ expect(result[:status]).to eq(0)
158
+ expect(result[:stdout]).to include('-sDEVICE=pdfwrite')
159
+ expect(result[:stdout]).to include('-sOutputFile=output.pdf')
160
+ expect(result[:stdout]).to include('-dBATCH')
161
+ expect(result[:stdout]).to include('-dNOPAUSE')
162
+ expect(result[:stdout]).to include('input.ps')
163
+ end
164
+ end
165
+ end