ruby_spriter 0.6.5

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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +217 -0
  4. data/Gemfile +17 -0
  5. data/LICENSE +21 -0
  6. data/README.md +561 -0
  7. data/bin/ruby_spriter +20 -0
  8. data/lib/ruby_spriter/cli.rb +249 -0
  9. data/lib/ruby_spriter/consolidator.rb +146 -0
  10. data/lib/ruby_spriter/dependency_checker.rb +174 -0
  11. data/lib/ruby_spriter/gimp_processor.rb +664 -0
  12. data/lib/ruby_spriter/metadata_manager.rb +116 -0
  13. data/lib/ruby_spriter/platform.rb +82 -0
  14. data/lib/ruby_spriter/processor.rb +251 -0
  15. data/lib/ruby_spriter/utils/file_helper.rb +57 -0
  16. data/lib/ruby_spriter/utils/output_formatter.rb +65 -0
  17. data/lib/ruby_spriter/utils/path_helper.rb +59 -0
  18. data/lib/ruby_spriter/version.rb +7 -0
  19. data/lib/ruby_spriter/video_processor.rb +139 -0
  20. data/lib/ruby_spriter.rb +31 -0
  21. data/ruby_spriter.gemspec +42 -0
  22. data/spec/fixtures/image_without_metadata.png +0 -0
  23. data/spec/fixtures/spritesheet_4x2.png +0 -0
  24. data/spec/fixtures/spritesheet_4x4.png +0 -0
  25. data/spec/fixtures/spritesheet_6x2.png +0 -0
  26. data/spec/fixtures/spritesheet_with_metadata.png +0 -0
  27. data/spec/fixtures/test_video.mp4 +0 -0
  28. data/spec/ruby_spriter/cli_spec.rb +1142 -0
  29. data/spec/ruby_spriter/consolidator_spec.rb +375 -0
  30. data/spec/ruby_spriter/dependency_checker_spec.rb +0 -0
  31. data/spec/ruby_spriter/gimp_processor_spec.rb +425 -0
  32. data/spec/ruby_spriter/metadata_manager_spec.rb +0 -0
  33. data/spec/ruby_spriter/platform_spec.rb +82 -0
  34. data/spec/ruby_spriter/processor_spec.rb +0 -0
  35. data/spec/ruby_spriter/utils/file_helper_spec.rb +71 -0
  36. data/spec/ruby_spriter/utils/output_formatter_spec.rb +0 -0
  37. data/spec/ruby_spriter/utils/path_helper_spec.rb +78 -0
  38. data/spec/ruby_spriter/video_processor_spec.rb +0 -0
  39. data/spec/spec_helper.rb +41 -0
  40. metadata +88 -0
@@ -0,0 +1,425 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubySpriter::GimpProcessor do
6
+ let(:gimp_path) { '/usr/bin/gimp' }
7
+ let(:test_image) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x2.png') }
8
+
9
+ describe '#initialize' do
10
+ it 'initializes with gimp_path and options' do
11
+ processor = described_class.new(gimp_path, scale_percent: 50)
12
+
13
+ expect(processor.gimp_path).to eq(gimp_path)
14
+ expect(processor.options[:scale_percent]).to eq(50)
15
+ end
16
+
17
+ it 'initializes with empty options by default' do
18
+ processor = described_class.new(gimp_path)
19
+
20
+ expect(processor.gimp_path).to eq(gimp_path)
21
+ expect(processor.options).to eq({})
22
+ end
23
+
24
+ it 'stores multiple options' do
25
+ processor = described_class.new(gimp_path, {
26
+ scale_percent: 50,
27
+ remove_bg: true,
28
+ scale_interpolation: 'nohalo',
29
+ sharpen: true
30
+ })
31
+
32
+ expect(processor.options[:scale_percent]).to eq(50)
33
+ expect(processor.options[:remove_bg]).to eq(true)
34
+ expect(processor.options[:scale_interpolation]).to eq('nohalo')
35
+ expect(processor.options[:sharpen]).to eq(true)
36
+ end
37
+ end
38
+
39
+ describe '#determine_operations' do
40
+ context 'with no operations requested' do
41
+ it 'returns empty array' do
42
+ processor = described_class.new(gimp_path, {})
43
+ operations = processor.send(:determine_operations)
44
+
45
+ expect(operations).to eq([])
46
+ end
47
+ end
48
+
49
+ context 'with scale only' do
50
+ it 'returns scale operation only' do
51
+ processor = described_class.new(gimp_path, scale_percent: 50)
52
+ operations = processor.send(:determine_operations)
53
+
54
+ expect(operations).to eq([:scale_image])
55
+ end
56
+ end
57
+
58
+ context 'with remove_bg only' do
59
+ it 'returns remove_background operation only' do
60
+ processor = described_class.new(gimp_path, remove_bg: true)
61
+ operations = processor.send(:determine_operations)
62
+
63
+ expect(operations).to eq([:remove_background])
64
+ end
65
+ end
66
+
67
+ context 'with both scale and remove_bg (auto-optimization)' do
68
+ it 'automatically does remove_bg first by default' do
69
+ processor = described_class.new(gimp_path, {
70
+ scale_percent: 50,
71
+ remove_bg: true,
72
+ operation_order: :scale_then_remove_bg # Default, but will be auto-optimized
73
+ })
74
+ operations = processor.send(:determine_operations)
75
+
76
+ # Auto-optimization: remove_bg should come first
77
+ expect(operations).to eq([:remove_background, :scale_image])
78
+ end
79
+
80
+ it 'respects explicit remove_bg_then_scale order' do
81
+ processor = described_class.new(gimp_path, {
82
+ scale_percent: 50,
83
+ remove_bg: true,
84
+ operation_order: :remove_bg_then_scale
85
+ })
86
+ operations = processor.send(:determine_operations)
87
+
88
+ expect(operations).to eq([:remove_background, :scale_image])
89
+ end
90
+
91
+ it 'respects explicit scale_then_remove_bg order when not auto-optimized' do
92
+ processor = described_class.new(gimp_path, {
93
+ scale_percent: 50,
94
+ remove_bg: true,
95
+ operation_order: :scale_then_remove_bg,
96
+ auto_optimize: false # If this were implemented
97
+ })
98
+ operations = processor.send(:determine_operations)
99
+
100
+ # With auto-optimization, this still gets optimized
101
+ expect(operations).to eq([:remove_background, :scale_image])
102
+ end
103
+ end
104
+
105
+ context 'operation order edge cases' do
106
+ it 'handles scale=nil, remove_bg=true' do
107
+ processor = described_class.new(gimp_path, {
108
+ scale_percent: nil,
109
+ remove_bg: true
110
+ })
111
+ operations = processor.send(:determine_operations)
112
+
113
+ expect(operations).to eq([:remove_background])
114
+ end
115
+
116
+ it 'handles scale=50, remove_bg=false' do
117
+ processor = described_class.new(gimp_path, {
118
+ scale_percent: 50,
119
+ remove_bg: false
120
+ })
121
+ operations = processor.send(:determine_operations)
122
+
123
+ expect(operations).to eq([:scale_image])
124
+ end
125
+ end
126
+ end
127
+
128
+ describe '#map_interpolation_method' do
129
+ let(:processor) { described_class.new(gimp_path) }
130
+
131
+ it 'maps "none" to GIMP NONE constant' do
132
+ result = processor.send(:map_interpolation_method, 'none')
133
+ expect(result).to eq('Gimp.InterpolationType.NONE')
134
+ end
135
+
136
+ it 'maps "linear" to GIMP LINEAR constant' do
137
+ result = processor.send(:map_interpolation_method, 'linear')
138
+ expect(result).to eq('Gimp.InterpolationType.LINEAR')
139
+ end
140
+
141
+ it 'maps "cubic" to GIMP CUBIC constant' do
142
+ result = processor.send(:map_interpolation_method, 'cubic')
143
+ expect(result).to eq('Gimp.InterpolationType.CUBIC')
144
+ end
145
+
146
+ it 'maps "nohalo" to GIMP NOHALO constant' do
147
+ result = processor.send(:map_interpolation_method, 'nohalo')
148
+ expect(result).to eq('Gimp.InterpolationType.NOHALO')
149
+ end
150
+
151
+ it 'maps "lohalo" to GIMP LOHALO constant' do
152
+ result = processor.send(:map_interpolation_method, 'lohalo')
153
+ expect(result).to eq('Gimp.InterpolationType.LOHALO')
154
+ end
155
+
156
+ it 'is case-insensitive' do
157
+ expect(processor.send(:map_interpolation_method, 'NOHALO')).to eq('Gimp.InterpolationType.NOHALO')
158
+ expect(processor.send(:map_interpolation_method, 'NoHalo')).to eq('Gimp.InterpolationType.NOHALO')
159
+ expect(processor.send(:map_interpolation_method, 'LINEAR')).to eq('Gimp.InterpolationType.LINEAR')
160
+ end
161
+
162
+ it 'accepts symbol input' do
163
+ result = processor.send(:map_interpolation_method, :nohalo)
164
+ expect(result).to eq('Gimp.InterpolationType.NOHALO')
165
+ end
166
+
167
+ it 'defaults to NOHALO for unknown methods' do
168
+ expect(processor.send(:map_interpolation_method, 'unknown')).to eq('Gimp.InterpolationType.NOHALO')
169
+ expect(processor.send(:map_interpolation_method, 'foo')).to eq('Gimp.InterpolationType.NOHALO')
170
+ expect(processor.send(:map_interpolation_method, '')).to eq('Gimp.InterpolationType.NOHALO')
171
+ end
172
+
173
+ it 'defaults to NOHALO for nil' do
174
+ result = processor.send(:map_interpolation_method, nil)
175
+ expect(result).to eq('Gimp.InterpolationType.NOHALO')
176
+ end
177
+ end
178
+
179
+ describe '#filter_gimp_output' do
180
+ let(:processor) { described_class.new(gimp_path) }
181
+
182
+ it 'filters GEGL-WARNING lines' do
183
+ output = "GEGL-WARNING: some warning\nUseful output\n"
184
+ result = processor.send(:filter_gimp_output, output)
185
+
186
+ expect(result).to eq("Useful output\n")
187
+ end
188
+
189
+ it 'filters gegl_tile_cache_destroy lines' do
190
+ output = "gegl_tile_cache_destroy: leaked tiles\nUseful output\n"
191
+ result = processor.send(:filter_gimp_output, output)
192
+
193
+ expect(result).to eq("Useful output\n")
194
+ end
195
+
196
+ it 'filters runtime check failed lines' do
197
+ output = "runtime check failed: something\nUseful output\n"
198
+ result = processor.send(:filter_gimp_output, output)
199
+
200
+ expect(result).to eq("Useful output\n")
201
+ end
202
+
203
+ it 'filters batch command executed successfully lines' do
204
+ output = "batch command executed successfully\nUseful output\n"
205
+ result = processor.send(:filter_gimp_output, output)
206
+
207
+ expect(result).to eq("Useful output\n")
208
+ end
209
+
210
+ it 'filters GEGL_DEBUG buffer-alloc lines' do
211
+ output = "GEGL_DEBUG: buffer-alloc details\nUseful output\n"
212
+ result = processor.send(:filter_gimp_output, output)
213
+
214
+ expect(result).to eq("Useful output\n")
215
+ end
216
+
217
+ it 'filters GeglBuffers leaked lines' do
218
+ output = "GeglBuffers leaked: 5\nUseful output\n"
219
+ result = processor.send(:filter_gimp_output, output)
220
+
221
+ expect(result).to eq("Useful output\n")
222
+ end
223
+
224
+ it 'filters EEEEeEeek lines' do
225
+ output = "EEEEeEeek! scary message\nUseful output\n"
226
+ result = processor.send(:filter_gimp_output, output)
227
+
228
+ expect(result).to eq("Useful output\n")
229
+ end
230
+
231
+ it 'filters empty lines' do
232
+ output = "Line 1\n\nLine 2\n \nLine 3\n"
233
+ result = processor.send(:filter_gimp_output, output)
234
+
235
+ expect(result).to eq("Line 1\nLine 2\nLine 3\n")
236
+ end
237
+
238
+ it 'filters multiple warning types in one output' do
239
+ output = <<~OUTPUT
240
+ GEGL-WARNING: warning 1
241
+ Useful line 1
242
+ gegl_tile_cache_destroy: leak
243
+ Useful line 2
244
+ runtime check failed
245
+ batch command executed successfully
246
+ Useful line 3
247
+ OUTPUT
248
+
249
+ result = processor.send(:filter_gimp_output, output)
250
+
251
+ expect(result).to eq("Useful line 1\nUseful line 2\nUseful line 3\n")
252
+ end
253
+
254
+ it 'returns empty string when all lines are filtered' do
255
+ output = <<~OUTPUT
256
+ GEGL-WARNING: warning
257
+ gegl_tile_cache_destroy: leak
258
+ runtime check failed
259
+ OUTPUT
260
+
261
+ result = processor.send(:filter_gimp_output, output)
262
+
263
+ expect(result).to eq("")
264
+ end
265
+
266
+ it 'preserves important error messages' do
267
+ output = "Error: File not found\nGEGL-WARNING: some warning\n"
268
+ result = processor.send(:filter_gimp_output, output)
269
+
270
+ expect(result).to eq("Error: File not found\n")
271
+ end
272
+ end
273
+
274
+ describe '#has_important_messages?' do
275
+ let(:processor) { described_class.new(gimp_path) }
276
+
277
+ it 'returns false for only filtered warnings' do
278
+ output = <<~OUTPUT
279
+ GEGL-WARNING: warning
280
+ gegl_tile_cache_destroy: leak
281
+ OUTPUT
282
+
283
+ expect(processor.send(:has_important_messages?, output)).to be false
284
+ end
285
+
286
+ it 'returns false for empty output' do
287
+ expect(processor.send(:has_important_messages?, "")).to be false
288
+ end
289
+
290
+ it 'returns false for only SUCCESS messages' do
291
+ output = "SUCCESS: Operation completed\n"
292
+
293
+ expect(processor.send(:has_important_messages?, output)).to be false
294
+ end
295
+
296
+ it 'returns false for SUCCESS messages mixed with filtered warnings' do
297
+ output = <<~OUTPUT
298
+ GEGL-WARNING: warning
299
+ SUCCESS: Operation completed
300
+ gegl_tile_cache_destroy: leak
301
+ OUTPUT
302
+
303
+ expect(processor.send(:has_important_messages?, output)).to be false
304
+ end
305
+
306
+ it 'returns true for error messages' do
307
+ output = "Error: Something went wrong\n"
308
+
309
+ expect(processor.send(:has_important_messages?, output)).to be true
310
+ end
311
+
312
+ it 'returns true for important messages mixed with warnings' do
313
+ output = <<~OUTPUT
314
+ GEGL-WARNING: warning
315
+ Error: File not found
316
+ gegl_tile_cache_destroy: leak
317
+ OUTPUT
318
+
319
+ expect(processor.send(:has_important_messages?, output)).to be true
320
+ end
321
+
322
+ it 'returns true for non-filtered, non-SUCCESS output' do
323
+ output = "Processing image...\nDone\n"
324
+
325
+ expect(processor.send(:has_important_messages?, output)).to be true
326
+ end
327
+ end
328
+
329
+ describe 'script generation' do
330
+ let(:processor) { described_class.new(gimp_path, scale_percent: 50, scale_interpolation: 'nohalo') }
331
+ let(:input_file) { '/path/to/input.png' }
332
+ let(:output_file) { '/path/to/output.png' }
333
+
334
+ describe '#generate_scale_script' do
335
+ it 'generates Python script with correct file paths' do
336
+ script = processor.send(:generate_scale_script, input_file, output_file, 50)
337
+
338
+ expect(script).to include('input.png')
339
+ expect(script).to include('output.png')
340
+ end
341
+
342
+ it 'includes correct scale percentage' do
343
+ script = processor.send(:generate_scale_script, input_file, output_file, 50)
344
+
345
+ # Percent is embedded directly and divided by 100 in Python: int(w * 50 / 100.0)
346
+ expect(script).to include('* 50 / 100.0')
347
+ end
348
+
349
+ it 'includes correct interpolation method' do
350
+ script = processor.send(:generate_scale_script, input_file, output_file, 50)
351
+
352
+ expect(script).to include('Gimp.InterpolationType.NOHALO')
353
+ end
354
+
355
+ it 'uses correct GIMP 3.x API' do
356
+ script = processor.send(:generate_scale_script, input_file, output_file, 50)
357
+
358
+ expect(script).to include('Gimp.file_load')
359
+ expect(script).to include('gimp-context-set-interpolation')
360
+ expect(script).to include('gimp-layer-scale')
361
+ end
362
+
363
+ it 'handles different scale percentages' do
364
+ script_25 = processor.send(:generate_scale_script, input_file, output_file, 25)
365
+ script_75 = processor.send(:generate_scale_script, input_file, output_file, 75)
366
+
367
+ expect(script_25).to include('* 25 / 100.0')
368
+ expect(script_75).to include('* 75 / 100.0')
369
+ end
370
+ end
371
+
372
+ describe '#generate_remove_bg_script' do
373
+ context 'with fuzzy select (default)' do
374
+ let(:processor_fuzzy) { described_class.new(gimp_path, remove_bg: true, fuzzy_select: true) }
375
+
376
+ it 'generates script with fuzzy select' do
377
+ script = processor_fuzzy.send(:generate_remove_bg_script, input_file, output_file)
378
+
379
+ expect(script).to include('gimp-image-select-contiguous-color')
380
+ end
381
+
382
+ it 'samples all four corners' do
383
+ script = processor_fuzzy.send(:generate_remove_bg_script, input_file, output_file)
384
+
385
+ # The procedure is looked up once, then used in a loop for all 4 corners
386
+ expect(script).to include('gimp-image-select-contiguous-color')
387
+ expect(script).to include('for i, (x, y) in enumerate(corners):')
388
+ # Verify corners array has 4 entries
389
+ expect(script).to include('(0, 0)') # Top-left
390
+ expect(script).to include('(w-1, 0)') # Top-right
391
+ expect(script).to include('(0, h-1)') # Bottom-left
392
+ expect(script).to include('(w-1, h-1)') # Bottom-right
393
+ end
394
+ end
395
+
396
+ context 'with global color select' do
397
+ let(:processor_global) { described_class.new(gimp_path, remove_bg: true, fuzzy_select: false) }
398
+
399
+ it 'generates script with global color select' do
400
+ script = processor_global.send(:generate_remove_bg_script, input_file, output_file)
401
+
402
+ expect(script).to include('gimp-image-select-color')
403
+ end
404
+ end
405
+
406
+ it 'includes file paths' do
407
+ processor_bg = described_class.new(gimp_path, remove_bg: true)
408
+ script = processor_bg.send(:generate_remove_bg_script, input_file, output_file)
409
+
410
+ expect(script).to include('input.png')
411
+ expect(script).to include('output.png')
412
+ end
413
+
414
+ it 'uses correct GIMP 3.x API' do
415
+ processor_bg = described_class.new(gimp_path, remove_bg: true)
416
+ script = processor_bg.send(:generate_remove_bg_script, input_file, output_file)
417
+
418
+ expect(script).to include('Gimp.file_load')
419
+ # Background removal selects corners directly (no inversion needed)
420
+ expect(script).to include('gimp-drawable-edit-clear')
421
+ expect(script).to include('gimp-selection-none')
422
+ end
423
+ end
424
+ end
425
+ end
File without changes
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubySpriter::Platform do
6
+ describe '.current' do
7
+ it 'returns a valid platform type' do
8
+ expect([:windows, :linux, :macos, :unknown]).to include(described_class.current)
9
+ end
10
+ end
11
+
12
+ describe '.windows?' do
13
+ it 'returns boolean' do
14
+ expect([true, false]).to include(described_class.windows?)
15
+ end
16
+ end
17
+
18
+ describe '.linux?' do
19
+ it 'returns boolean' do
20
+ expect([true, false]).to include(described_class.linux?)
21
+ end
22
+ end
23
+
24
+ describe '.macos?' do
25
+ it 'returns boolean' do
26
+ expect([true, false]).to include(described_class.macos?)
27
+ end
28
+ end
29
+
30
+ describe '.default_gimp_path' do
31
+ it 'returns a string path' do
32
+ expect(described_class.default_gimp_path).to be_a(String)
33
+ end
34
+
35
+ it 'returns platform-appropriate path' do
36
+ path = described_class.default_gimp_path
37
+
38
+ if described_class.windows?
39
+ expect(path).to match(/GIMP/)
40
+ expect(path).to match(/\.exe$/)
41
+ else
42
+ expect(path).to start_with('/')
43
+ end
44
+ end
45
+ end
46
+
47
+ describe '.alternative_gimp_paths' do
48
+ it 'returns an array' do
49
+ expect(described_class.alternative_gimp_paths).to be_an(Array)
50
+ end
51
+
52
+ it 'contains only strings' do
53
+ described_class.alternative_gimp_paths.each do |path|
54
+ expect(path).to be_a(String)
55
+ end
56
+ end
57
+ end
58
+
59
+ describe '.imagemagick_convert_cmd' do
60
+ it 'returns appropriate command for platform' do
61
+ cmd = described_class.imagemagick_convert_cmd
62
+
63
+ if described_class.windows?
64
+ expect(cmd).to eq('magick convert')
65
+ else
66
+ expect(cmd).to eq('convert')
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '.imagemagick_identify_cmd' do
72
+ it 'returns appropriate command for platform' do
73
+ cmd = described_class.imagemagick_identify_cmd
74
+
75
+ if described_class.windows?
76
+ expect(cmd).to eq('magick identify')
77
+ else
78
+ expect(cmd).to eq('identify')
79
+ end
80
+ end
81
+ end
82
+ end
File without changes
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubySpriter::Utils::FileHelper do
6
+ describe '.spritesheet_filename' do
7
+ it 'generates correct filename from video' do
8
+ result = described_class.spritesheet_filename('/path/to/video.mp4')
9
+ expect(result).to eq('/path/to/video_spritesheet.png')
10
+ end
11
+
12
+ it 'handles different extensions' do
13
+ result = described_class.spritesheet_filename('C:\\videos\\clip.avi')
14
+ expect(result).to match(/clip_spritesheet\.png$/)
15
+ end
16
+ end
17
+
18
+ describe '.output_filename' do
19
+ it 'generates filename with suffix' do
20
+ result = described_class.output_filename('/path/to/image.png', 'scaled')
21
+ expect(result).to eq('/path/to/image-scaled.png')
22
+ end
23
+
24
+ it 'preserves directory path' do
25
+ result = described_class.output_filename('/some/deep/path/file.png', 'nobg')
26
+ expect(result).to start_with('/some/deep/path/')
27
+ end
28
+ end
29
+
30
+ describe '.format_size' do
31
+ it 'formats bytes correctly' do
32
+ expect(described_class.format_size(500)).to eq('500 bytes')
33
+ end
34
+
35
+ it 'formats kilobytes correctly' do
36
+ expect(described_class.format_size(2048)).to eq('2.0 KB')
37
+ end
38
+
39
+ it 'formats megabytes correctly' do
40
+ expect(described_class.format_size(5_242_880)).to eq('5.0 MB')
41
+ end
42
+ end
43
+
44
+ describe '.validate_exists!' do
45
+ it 'raises error for non-existent file' do
46
+ expect {
47
+ described_class.validate_exists!('/nonexistent/file.txt')
48
+ }.to raise_error(RubySpriter::ValidationError, /File not found/)
49
+ end
50
+
51
+ it 'does not raise for existing file' do
52
+ file = File.join(@test_dir, 'test.txt')
53
+ File.write(file, 'test')
54
+
55
+ expect {
56
+ described_class.validate_exists!(file)
57
+ }.not_to raise_error
58
+ end
59
+ end
60
+
61
+ describe '.validate_readable!' do
62
+ it 'validates file exists and is readable' do
63
+ file = File.join(@test_dir, 'test.txt')
64
+ File.write(file, 'test')
65
+
66
+ expect {
67
+ described_class.validate_readable!(file)
68
+ }.not_to raise_error
69
+ end
70
+ end
71
+ end
File without changes
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubySpriter::Utils::PathHelper do
6
+ describe '.quote_path' do
7
+ context 'on Windows' do
8
+ before do
9
+ allow(RubySpriter::Platform).to receive(:windows?).and_return(true)
10
+ end
11
+
12
+ it 'wraps path in double quotes' do
13
+ expect(described_class.quote_path('C:\\test\\file.txt')).to eq('"C:\\test\\file.txt"')
14
+ end
15
+ end
16
+
17
+ context 'on Unix-like systems' do
18
+ before do
19
+ allow(RubySpriter::Platform).to receive(:windows?).and_return(false)
20
+ end
21
+
22
+ it 'wraps path in single quotes' do
23
+ expect(described_class.quote_path('/test/file.txt')).to eq("'/test/file.txt'")
24
+ end
25
+
26
+ it 'escapes single quotes in path' do
27
+ result = described_class.quote_path("/test/file's.txt")
28
+ # Should wrap in single quotes and escape internal single quotes
29
+ expect(result).to start_with("'")
30
+ expect(result).to end_with("'")
31
+ expect(result).to include("\\'") # Should contain escaped single quote
32
+ end
33
+ end
34
+ end
35
+
36
+ describe '.normalize_for_python' do
37
+ it 'returns absolute path' do
38
+ result = described_class.normalize_for_python('.')
39
+ # Should return an absolute path (Unix: starts with /, Windows: starts with drive letter)
40
+ is_unix_absolute = result.start_with?('/')
41
+ is_windows_absolute = result.match?(/^[A-Z]:/i)
42
+ expect(is_unix_absolute || is_windows_absolute).to be true
43
+ end
44
+
45
+ context 'on Windows' do
46
+ before do
47
+ allow(RubySpriter::Platform).to receive(:windows?).and_return(true)
48
+ end
49
+
50
+ it 'converts backslashes to forward slashes' do
51
+ allow(File).to receive(:absolute_path).and_return('C:\\test\\file.txt')
52
+ expect(described_class.normalize_for_python('file.txt')).to eq('C:/test/file.txt')
53
+ end
54
+ end
55
+ end
56
+
57
+ describe '.to_native' do
58
+ context 'on Windows' do
59
+ before do
60
+ allow(RubySpriter::Platform).to receive(:windows?).and_return(true)
61
+ end
62
+
63
+ it 'converts forward slashes to backslashes' do
64
+ expect(described_class.to_native('C:/test/file.txt')).to eq('C:\\test\\file.txt')
65
+ end
66
+ end
67
+
68
+ context 'on Unix-like systems' do
69
+ before do
70
+ allow(RubySpriter::Platform).to receive(:windows?).and_return(false)
71
+ end
72
+
73
+ it 'converts backslashes to forward slashes' do
74
+ expect(described_class.to_native('C:\\test\\file.txt')).to eq('C:/test/file.txt')
75
+ end
76
+ end
77
+ end
78
+ end
File without changes