ruby_spriter 0.6.7 → 0.7.0.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -3
  3. data/CHANGELOG.md +1035 -405
  4. data/Gemfile +17 -17
  5. data/LICENSE +21 -21
  6. data/README.md +183 -902
  7. data/bin/ruby_spriter +20 -20
  8. data/lib/ruby_spriter/background_sampler.rb +140 -0
  9. data/lib/ruby_spriter/batch_processor.rb +268 -212
  10. data/lib/ruby_spriter/cell_cleanup_config.rb +21 -0
  11. data/lib/ruby_spriter/cell_cleanup_gimp_script.rb +123 -0
  12. data/lib/ruby_spriter/cell_cleanup_processor.rb +230 -0
  13. data/lib/ruby_spriter/cli.rb +676 -612
  14. data/lib/ruby_spriter/compression_manager.rb +101 -101
  15. data/lib/ruby_spriter/consolidator.rb +179 -179
  16. data/lib/ruby_spriter/dependency_checker.rb +224 -174
  17. data/lib/ruby_spriter/ghost_edge_cleaner.rb +164 -0
  18. data/lib/ruby_spriter/gimp_processor.rb +1188 -667
  19. data/lib/ruby_spriter/metadata_manager.rb +117 -116
  20. data/lib/ruby_spriter/platform.rb +137 -82
  21. data/lib/ruby_spriter/processor.rb +1230 -886
  22. data/lib/ruby_spriter/smoke_detector.rb +223 -0
  23. data/lib/ruby_spriter/threshold_stepper.rb +227 -0
  24. data/lib/ruby_spriter/utils/file_helper.rb +82 -82
  25. data/lib/ruby_spriter/utils/image_helper.rb +16 -0
  26. data/lib/ruby_spriter/utils/output_formatter.rb +65 -65
  27. data/lib/ruby_spriter/utils/path_helper.rb +59 -59
  28. data/lib/ruby_spriter/utils/spritesheet_splitter.rb +86 -86
  29. data/lib/ruby_spriter/version.rb +6 -7
  30. data/lib/ruby_spriter/video_processor.rb +357 -139
  31. data/lib/ruby_spriter.rb +38 -34
  32. data/ruby_spriter.gemspec +44 -42
  33. data/spec/fixtures/centered_sprite_with_inner_bg.png +0 -0
  34. data/spec/fixtures/centered_sprite_with_inner_bg_inner_removed.png +0 -0
  35. data/spec/fixtures/centered_sprite_with_inner_bg_threshold_stepped.png +0 -0
  36. data/spec/fixtures/complex_background_sprite.png +0 -0
  37. data/spec/fixtures/death - bloodborne rekconing.mp4 +0 -0
  38. data/spec/fixtures/death - bloodborne rekconing_spritesheet-nobg-global.png +0 -0
  39. data/spec/fixtures/death - bloodborne rekconing_spritesheet.png +0 -0
  40. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431-nobg-global.png +0 -0
  41. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431.png +0 -0
  42. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738-nobg-global.png +0 -0
  43. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738.png +0 -0
  44. data/spec/fixtures/has_inner_bg.png +0 -0
  45. data/spec/fixtures/has_small_inner_bg.png +0 -0
  46. data/spec/fixtures/smoke_effect_sprite.png +0 -0
  47. data/spec/fixtures/spritesheet_with_metadata.png +0 -0
  48. data/spec/fixtures/test_sprite.png +0 -0
  49. data/spec/fixtures/test_sprite_smoke_processed.png +0 -0
  50. data/spec/fixtures/test_video_spritesheet.png +0 -0
  51. data/spec/fixtures/transparent_bg_sprite.png +0 -0
  52. data/spec/fixtures/walk_north_sprite-sheet.png +0 -0
  53. data/spec/integration/no_fuzzy_mode_spec.rb +100 -0
  54. data/spec/ruby_spriter/batch_processor_spec.rb +509 -200
  55. data/spec/ruby_spriter/cli_spec.rb +2026 -1892
  56. data/spec/ruby_spriter/compression_manager_spec.rb +157 -157
  57. data/spec/ruby_spriter/consolidator_spec.rb +538 -538
  58. data/spec/ruby_spriter/gimp_processor_spec.rb +523 -425
  59. data/spec/ruby_spriter/platform_spec.rb +92 -82
  60. data/spec/ruby_spriter/processor_spec.rb +911 -735
  61. data/spec/ruby_spriter/utils/file_helper_spec.rb +150 -150
  62. data/spec/ruby_spriter/utils/path_helper_spec.rb +78 -78
  63. data/spec/ruby_spriter/utils/spritesheet_splitter_spec.rb +104 -104
  64. data/spec/ruby_spriter/video_processor_spec.rb +346 -29
  65. data/spec/spec_helper.rb +41 -41
  66. data/spec/tmp/cli_test_output.png +0 -0
  67. data/spec/tmp/cli_test_output_20251105_114647_395.png +0 -0
  68. data/spec/tmp/combined_test.png +0 -0
  69. data/spec/tmp/compat_test.png +0 -0
  70. data/spec/tmp/compat_test_20251030_174233_361.png +0 -0
  71. data/spec/tmp/final_all_features.png +0 -0
  72. data/spec/tmp/final_test_all_features.png +0 -0
  73. data/spec/tmp/full_pipeline_test.png +0 -0
  74. data/spec/tmp/inner_test.png +0 -0
  75. data/spec/tmp/integration_test.png +0 -0
  76. data/spec/tmp/validation_test.png +0 -0
  77. data/spec/unit/background_sampler_spec.rb +132 -0
  78. data/spec/unit/cell_cleanup_config_spec.rb +32 -0
  79. data/spec/unit/cell_cleanup_gimp_script_spec.rb +59 -0
  80. data/spec/unit/cell_cleanup_processor_spec.rb +261 -0
  81. data/spec/unit/ghost_edge_cleaner_spec.rb +223 -0
  82. data/spec/unit/gimp_processor_single_point_selection_spec.rb +81 -0
  83. data/spec/unit/smoke_detector_spec.rb +246 -0
  84. data/spec/unit/threshold_stepper_spec.rb +195 -0
  85. metadata +56 -10
@@ -1,538 +1,538 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
- require 'securerandom'
5
-
6
- RSpec.describe RubySpriter::Consolidator do
7
- let(:spritesheet1) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x2.png') }
8
- let(:spritesheet2) { File.join(__dir__, '..', 'fixtures', 'spritesheet_6x2.png') }
9
- let(:spritesheet3) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x4.png') }
10
- let(:output_file) { File.join($test_temp_dir, 'consolidated.png') }
11
-
12
- describe '#initialize' do
13
- it 'initializes with empty options by default' do
14
- consolidator = described_class.new
15
-
16
- expect(consolidator.options).to eq({})
17
- end
18
-
19
- it 'initializes with provided options' do
20
- consolidator = described_class.new(debug: true, validate_columns: false)
21
-
22
- expect(consolidator.options[:debug]).to be true
23
- expect(consolidator.options[:validate_columns]).to be false
24
- end
25
- end
26
-
27
- describe '#consolidate' do
28
- let(:consolidator) { described_class.new }
29
-
30
- # Helper to set up common mocks for successful consolidation
31
- def stub_consolidation_success
32
- allow(RubySpriter::MetadataManager).to receive(:embed)
33
- allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
34
- allow(File).to receive(:rename)
35
- allow(File).to receive(:exist?).and_call_original
36
- allow(File).to receive(:exist?).with(/consolidated.*temp/).and_return(true)
37
- allow(File).to receive(:delete)
38
- end
39
-
40
- context 'with file validation' do
41
- it 'raises error when given 0 files' do
42
- expect {
43
- consolidator.consolidate([], output_file)
44
- }.to raise_error(RubySpriter::ValidationError, /Need at least 2 files/)
45
- end
46
-
47
- it 'raises error when given only 1 file' do
48
- expect {
49
- consolidator.consolidate([spritesheet1], output_file)
50
- }.to raise_error(RubySpriter::ValidationError, /Need at least 2 files/)
51
- end
52
-
53
- it 'accepts 2 files minimum' do
54
- stub_consolidation_success
55
- allow(RubySpriter::MetadataManager).to receive(:read).and_return(
56
- { columns: 2, rows: 2, frames: 4 }
57
- )
58
- allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
59
- allow(File).to receive(:size).and_return(1000)
60
-
61
- expect {
62
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
63
- }.not_to raise_error
64
- end
65
-
66
- it 'raises error when file does not exist' do
67
- non_existent = 'non_existent.png'
68
-
69
- expect {
70
- consolidator.consolidate([spritesheet1, non_existent], output_file)
71
- }.to raise_error(RubySpriter::ValidationError, /not found/)
72
- end
73
- end
74
-
75
- context 'with metadata reading' do
76
- it 'reads metadata from all files' do
77
- stub_consolidation_success
78
- metadata1 = { columns: 2, rows: 2, frames: 4 }
79
- metadata2 = { columns: 2, rows: 3, frames: 6 }
80
-
81
- expect(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(metadata1)
82
- expect(RubySpriter::MetadataManager).to receive(:read).with(spritesheet2).and_return(metadata2)
83
-
84
- allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
85
- allow(File).to receive(:size).and_return(1000)
86
-
87
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
88
- end
89
-
90
- it 'raises ValidationError when file missing metadata' do
91
- allow(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(nil)
92
-
93
- expect {
94
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
95
- }.to raise_error(RubySpriter::ValidationError, /missing metadata/)
96
- end
97
-
98
- it 'raises error with helpful message about Ruby Spriter spritesheets' do
99
- allow(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(nil)
100
-
101
- expect {
102
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
103
- }.to raise_error(RubySpriter::ValidationError, /All files must be Ruby Spriter spritesheets/)
104
- end
105
- end
106
-
107
- context 'with column validation enabled (default)' do
108
- let(:consolidator) { described_class.new(validate_columns: true) }
109
-
110
- it 'passes when all files have same column count' do
111
- stub_consolidation_success
112
- metadata1 = { columns: 2, rows: 2, frames: 4 }
113
- metadata2 = { columns: 2, rows: 3, frames: 6 }
114
-
115
- allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
116
- allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
117
- allow(File).to receive(:size).and_return(1000)
118
-
119
- expect {
120
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
121
- }.not_to raise_error
122
- end
123
-
124
- it 'raises error when column counts do not match' do
125
- metadata1 = { columns: 2, rows: 2, frames: 4 }
126
- metadata2 = { columns: 4, rows: 1, frames: 4 }
127
-
128
- allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
129
-
130
- expect {
131
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
132
- }.to raise_error(RubySpriter::ValidationError, /Column count mismatch/)
133
- end
134
-
135
- it 'error message shows expected and actual column counts' do
136
- metadata1 = { columns: 2, rows: 2, frames: 4 }
137
- metadata2 = { columns: 4, rows: 1, frames: 4 }
138
-
139
- allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
140
-
141
- expect {
142
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
143
- }.to raise_error(RubySpriter::ValidationError, /Expected 2, found 4/)
144
- end
145
-
146
- it 'error message suggests --no-validate-columns flag' do
147
- metadata1 = { columns: 2, rows: 2, frames: 4 }
148
- metadata2 = { columns: 4, rows: 1, frames: 4 }
149
-
150
- allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
151
-
152
- expect {
153
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
154
- }.to raise_error(RubySpriter::ValidationError, /--no-validate-columns/)
155
- end
156
- end
157
-
158
- context 'with column validation disabled' do
159
- let(:consolidator) { described_class.new(validate_columns: false) }
160
- before { stub_consolidation_success }
161
-
162
- it 'allows mismatched column counts' do
163
- metadata1 = { columns: 2, rows: 2, frames: 4 }
164
- metadata2 = { columns: 4, rows: 1, frames: 4 }
165
-
166
- allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
167
- allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
168
- allow(File).to receive(:size).and_return(1000)
169
-
170
- expect {
171
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
172
- }.not_to raise_error
173
- end
174
- end
175
-
176
- context 'with ImageMagick consolidation' do
177
- before do
178
- stub_consolidation_success
179
- allow(RubySpriter::MetadataManager).to receive(:read).and_return(
180
- { columns: 2, rows: 2, frames: 4 }
181
- )
182
- allow(File).to receive(:size).and_return(1000)
183
- end
184
-
185
- it 'calls ImageMagick with correct command' do
186
- expect(Open3).to receive(:capture3) do |cmd|
187
- expect(cmd).to include('convert')
188
- expect(cmd).to include('-append')
189
- expect(cmd).to include(spritesheet1)
190
- expect(cmd).to include(spritesheet2)
191
- expect(cmd).to include(output_file)
192
- ['', '', instance_double(Process::Status, success?: true)]
193
- end
194
-
195
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
196
- end
197
-
198
- it 'uses -append flag for vertical stacking' do
199
- expect(Open3).to receive(:capture3) do |cmd|
200
- expect(cmd).to include('-append')
201
- ['', '', instance_double(Process::Status, success?: true)]
202
- end
203
-
204
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
205
- end
206
-
207
- it 'raises ProcessingError when ImageMagick fails' do
208
- allow(Open3).to receive(:capture3).and_return(
209
- ['', 'ImageMagick error', instance_double(Process::Status, success?: false)]
210
- )
211
-
212
- expect {
213
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
214
- }.to raise_error(RubySpriter::ProcessingError, /ImageMagick consolidation failed/)
215
- end
216
-
217
- it 'shows debug output when debug option enabled' do
218
- consolidator = described_class.new(debug: true)
219
-
220
- allow(RubySpriter::MetadataManager).to receive(:read).and_return(
221
- { columns: 2, rows: 2, frames: 4 }
222
- )
223
- allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
224
- allow(File).to receive(:size).and_return(1000)
225
-
226
- expect {
227
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
228
- }.to output(/DEBUG: ImageMagick command/).to_stdout
229
- end
230
- end
231
-
232
- context 'with successful consolidation' do
233
- before do
234
- stub_consolidation_success
235
- allow(RubySpriter::MetadataManager).to receive(:read).and_return(
236
- { columns: 2, rows: 2, frames: 4 },
237
- { columns: 2, rows: 3, frames: 6 }
238
- )
239
- allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
240
- allow(File).to receive(:size).and_return(5000)
241
- end
242
-
243
- it 'calculates correct total frames' do
244
- result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
245
-
246
- expect(result[:frames]).to eq(10) # 4 + 6
247
- end
248
-
249
- it 'calculates correct row count' do
250
- result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
251
-
252
- # 10 frames / 2 columns = 5 rows
253
- expect(result[:rows]).to eq(5)
254
- end
255
-
256
- it 'uses columns from first spritesheet' do
257
- result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
258
-
259
- expect(result[:columns]).to eq(2)
260
- end
261
-
262
- it 'embeds metadata in output file' do
263
- expect(RubySpriter::MetadataManager).to receive(:embed).with(
264
- anything,
265
- output_file,
266
- hash_including(columns: 2, rows: 5, frames: 10)
267
- )
268
-
269
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
270
- end
271
-
272
- it 'returns hash with correct keys' do
273
- result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
274
-
275
- expect(result).to include(
276
- output_file: output_file,
277
- columns: 2,
278
- rows: 5,
279
- frames: 10,
280
- size: 5000
281
- )
282
- end
283
-
284
- it 'returns correct file size' do
285
- result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
286
-
287
- expect(result[:size]).to eq(5000)
288
- end
289
- end
290
-
291
- context 'with 3 spritesheets' do
292
- before do
293
- stub_consolidation_success
294
- allow(RubySpriter::MetadataManager).to receive(:read).and_return(
295
- { columns: 4, rows: 1, frames: 4 },
296
- { columns: 4, rows: 1, frames: 4 },
297
- { columns: 4, rows: 1, frames: 4 }
298
- )
299
- allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
300
- allow(File).to receive(:size).and_return(8000)
301
- end
302
-
303
- it 'successfully consolidates all 3 files' do
304
- expect(Open3).to receive(:capture3) do |cmd|
305
- expect(cmd).to include(spritesheet1)
306
- expect(cmd).to include(spritesheet2)
307
- expect(cmd).to include(spritesheet3)
308
- ['', '', instance_double(Process::Status, success?: true)]
309
- end
310
-
311
- consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
312
- end
313
-
314
- it 'calculates correct total frames for 3 files' do
315
- result = consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
316
-
317
- expect(result[:frames]).to eq(12) # 4 + 4 + 4
318
- end
319
-
320
- it 'calculates correct row count for 3 files' do
321
- result = consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
322
-
323
- # 12 frames / 4 columns = 3 rows
324
- expect(result[:rows]).to eq(3)
325
- end
326
- end
327
-
328
- context 'with output display' do
329
- before do
330
- stub_consolidation_success
331
- allow(RubySpriter::MetadataManager).to receive(:read).and_return(
332
- { columns: 2, rows: 2, frames: 4 },
333
- { columns: 2, rows: 3, frames: 6 }
334
- )
335
- allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
336
- allow(File).to receive(:size).and_return(5000)
337
- end
338
-
339
- it 'displays success message' do
340
- expect {
341
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
342
- }.to output(/Consolidated spritesheet created/).to_stdout
343
- end
344
-
345
- it 'displays output file path' do
346
- expect {
347
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
348
- }.to output(/Output:.*consolidated\.png/).to_stdout
349
- end
350
-
351
- it 'displays grid layout information' do
352
- expect {
353
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
354
- }.to output(/Grid Layout:.*Columns: 2.*Rows: 5.*Total Frames: 10/m).to_stdout
355
- end
356
-
357
- it 'displays Godot AnimatedSprite2D settings' do
358
- expect {
359
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
360
- }.to output(/Godot AnimatedSprite2D Settings:.*HFrames = 2.*VFrames = 5/m).to_stdout
361
- end
362
-
363
- it 'displays source breakdown with frame counts' do
364
- expect {
365
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
366
- }.to output(/Source Breakdown:.*spritesheet_4x2\.png.*grid \(4 frames\).*spritesheet_6x2\.png.*grid \(6 frames\)/m).to_stdout
367
- end
368
-
369
- it 'displays number of combined spritesheets' do
370
- expect {
371
- consolidator.consolidate([spritesheet1, spritesheet2], output_file)
372
- }.to output(/Combined 2 spritesheets/).to_stdout
373
- end
374
- end
375
- end
376
-
377
- describe '#find_spritesheets_in_directory' do
378
- let(:consolidator) { described_class.new }
379
- let(:image_without_meta) { File.join(__dir__, '..', 'fixtures', 'image_without_metadata.png') }
380
-
381
- # Helper to create a unique test directory for each test
382
- def create_test_dir
383
- dir = File.join($test_temp_dir, "consolidate_test_#{SecureRandom.hex(8)}")
384
- FileUtils.mkdir_p(dir)
385
- dir
386
- end
387
-
388
- it 'finds all PNG files with metadata in directory' do
389
- test_dir = create_test_dir
390
- # Copy spritesheets with metadata to test directory
391
- sprite1_path = File.join(test_dir, 'sprite1.png')
392
- sprite2_path = File.join(test_dir, 'sprite2.png')
393
- FileUtils.cp(spritesheet1, sprite1_path)
394
- FileUtils.cp(spritesheet2, sprite2_path)
395
-
396
- # Mock metadata reading to return valid metadata for these files
397
- allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
398
- allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
399
- { columns: 2, rows: 2, frames: 4, version: '0.6' }
400
- )
401
- allow(RubySpriter::MetadataManager).to receive(:read).with(sprite2_path).and_return(
402
- { columns: 2, rows: 3, frames: 6, version: '0.6' }
403
- )
404
-
405
- found_files = consolidator.find_spritesheets_in_directory(test_dir)
406
-
407
- expect(found_files.length).to eq(2)
408
- expect(found_files).to include(sprite1_path)
409
- expect(found_files).to include(sprite2_path)
410
- end
411
-
412
- it 'excludes PNG files without metadata' do
413
- test_dir = create_test_dir
414
- # Copy two spritesheets with metadata and one image without
415
- sprite1_path = File.join(test_dir, 'sprite1.png')
416
- sprite2_path = File.join(test_dir, 'sprite2.png')
417
- no_meta_path = File.join(test_dir, 'no_meta.png')
418
- FileUtils.cp(spritesheet1, sprite1_path)
419
- FileUtils.cp(spritesheet2, sprite2_path)
420
- FileUtils.cp(image_without_meta, no_meta_path)
421
-
422
- # Mock metadata reading
423
- allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
424
- allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
425
- { columns: 2, rows: 2, frames: 4, version: '0.6' }
426
- )
427
- allow(RubySpriter::MetadataManager).to receive(:read).with(sprite2_path).and_return(
428
- { columns: 2, rows: 3, frames: 6, version: '0.6' }
429
- )
430
- allow(RubySpriter::MetadataManager).to receive(:read).with(no_meta_path).and_return(nil)
431
-
432
- found_files = consolidator.find_spritesheets_in_directory(test_dir)
433
-
434
- expect(found_files.length).to eq(2)
435
- expect(found_files).to include(sprite1_path)
436
- expect(found_files).to include(sprite2_path)
437
- expect(found_files).not_to include(no_meta_path)
438
- end
439
-
440
- it 'returns files sorted alphabetically' do
441
- test_dir = create_test_dir
442
- # Copy spritesheets with names that test alphabetical sorting
443
- c_sprite_path = File.join(test_dir, 'c_sprite.png')
444
- a_sprite_path = File.join(test_dir, 'a_sprite.png')
445
- b_sprite_path = File.join(test_dir, 'b_sprite.png')
446
- FileUtils.cp(spritesheet1, c_sprite_path)
447
- FileUtils.cp(spritesheet2, a_sprite_path)
448
- FileUtils.cp(spritesheet3, b_sprite_path)
449
-
450
- # Mock metadata reading
451
- allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
452
- allow(RubySpriter::MetadataManager).to receive(:read).with(c_sprite_path).and_return(
453
- { columns: 4, rows: 1, frames: 4, version: '0.6' }
454
- )
455
- allow(RubySpriter::MetadataManager).to receive(:read).with(a_sprite_path).and_return(
456
- { columns: 2, rows: 3, frames: 6, version: '0.6' }
457
- )
458
- allow(RubySpriter::MetadataManager).to receive(:read).with(b_sprite_path).and_return(
459
- { columns: 4, rows: 4, frames: 16, version: '0.6' }
460
- )
461
-
462
- found_files = consolidator.find_spritesheets_in_directory(test_dir)
463
-
464
- expect(found_files.length).to eq(3)
465
- expect(found_files[0]).to end_with('a_sprite.png')
466
- expect(found_files[1]).to end_with('b_sprite.png')
467
- expect(found_files[2]).to end_with('c_sprite.png')
468
- end
469
-
470
- it 'raises error when directory does not exist' do
471
- expect {
472
- consolidator.find_spritesheets_in_directory('nonexistent_directory')
473
- }.to raise_error(RubySpriter::ValidationError, /Directory not found/)
474
- end
475
-
476
- it 'raises error when no PNG files with metadata found' do
477
- # Create empty directory
478
- empty_dir = File.join($test_temp_dir, 'empty_dir')
479
- FileUtils.mkdir_p(empty_dir)
480
-
481
- expect {
482
- consolidator.find_spritesheets_in_directory(empty_dir)
483
- }.to raise_error(RubySpriter::ValidationError, /No PNG files with spritesheet metadata found/)
484
- end
485
-
486
- it 'raises error when directory has PNGs but none with metadata' do
487
- test_dir = create_test_dir
488
- # Copy only image without metadata
489
- FileUtils.cp(image_without_meta, File.join(test_dir, 'no_meta.png'))
490
-
491
- expect {
492
- consolidator.find_spritesheets_in_directory(test_dir)
493
- }.to raise_error(RubySpriter::ValidationError, /No PNG files with spritesheet metadata found/)
494
- end
495
-
496
- it 'handles directory with mixed file types' do
497
- test_dir = create_test_dir
498
- # Copy spritesheets and create non-PNG files
499
- sprite1_path = File.join(test_dir, 'sprite1.png')
500
- sprite2_path = File.join(test_dir, 'sprite2.png')
501
- FileUtils.cp(spritesheet1, sprite1_path)
502
- FileUtils.cp(spritesheet2, sprite2_path)
503
- File.write(File.join(test_dir, 'readme.txt'), 'test')
504
- File.write(File.join(test_dir, 'data.json'), '{}')
505
-
506
- # Mock metadata reading
507
- allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
508
- allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
509
- { columns: 2, rows: 2, frames: 4, version: '0.6' }
510
- )
511
- allow(RubySpriter::MetadataManager).to receive(:read).with(sprite2_path).and_return(
512
- { columns: 2, rows: 3, frames: 6, version: '0.6' }
513
- )
514
-
515
- found_files = consolidator.find_spritesheets_in_directory(test_dir)
516
-
517
- expect(found_files.length).to eq(2)
518
- expect(found_files).to all(end_with('.png'))
519
- end
520
-
521
- it 'requires at least 2 spritesheets' do
522
- test_dir = create_test_dir
523
- # Copy only one spritesheet
524
- sprite1_path = File.join(test_dir, 'sprite1.png')
525
- FileUtils.cp(spritesheet1, sprite1_path)
526
-
527
- # Mock metadata reading
528
- allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
529
- allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
530
- { columns: 2, rows: 2, frames: 4, version: '0.6' }
531
- )
532
-
533
- expect {
534
- consolidator.find_spritesheets_in_directory(test_dir)
535
- }.to raise_error(RubySpriter::ValidationError, /Found only 1 spritesheet, need at least 2/)
536
- end
537
- end
538
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'securerandom'
5
+
6
+ RSpec.describe RubySpriter::Consolidator do
7
+ let(:spritesheet1) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x2.png') }
8
+ let(:spritesheet2) { File.join(__dir__, '..', 'fixtures', 'spritesheet_6x2.png') }
9
+ let(:spritesheet3) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x4.png') }
10
+ let(:output_file) { File.join($test_temp_dir, 'consolidated.png') }
11
+
12
+ describe '#initialize' do
13
+ it 'initializes with empty options by default' do
14
+ consolidator = described_class.new
15
+
16
+ expect(consolidator.options).to eq({})
17
+ end
18
+
19
+ it 'initializes with provided options' do
20
+ consolidator = described_class.new(debug: true, validate_columns: false)
21
+
22
+ expect(consolidator.options[:debug]).to be true
23
+ expect(consolidator.options[:validate_columns]).to be false
24
+ end
25
+ end
26
+
27
+ describe '#consolidate' do
28
+ let(:consolidator) { described_class.new }
29
+
30
+ # Helper to set up common mocks for successful consolidation
31
+ def stub_consolidation_success
32
+ allow(RubySpriter::MetadataManager).to receive(:embed)
33
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
34
+ allow(File).to receive(:rename)
35
+ allow(File).to receive(:exist?).and_call_original
36
+ allow(File).to receive(:exist?).with(/consolidated.*temp/).and_return(true)
37
+ allow(File).to receive(:delete)
38
+ end
39
+
40
+ context 'with file validation' do
41
+ it 'raises error when given 0 files' do
42
+ expect {
43
+ consolidator.consolidate([], output_file)
44
+ }.to raise_error(RubySpriter::ValidationError, /Need at least 2 files/)
45
+ end
46
+
47
+ it 'raises error when given only 1 file' do
48
+ expect {
49
+ consolidator.consolidate([spritesheet1], output_file)
50
+ }.to raise_error(RubySpriter::ValidationError, /Need at least 2 files/)
51
+ end
52
+
53
+ it 'accepts 2 files minimum' do
54
+ stub_consolidation_success
55
+ allow(RubySpriter::MetadataManager).to receive(:read).and_return(
56
+ { columns: 2, rows: 2, frames: 4 }
57
+ )
58
+ allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
59
+ allow(File).to receive(:size).and_return(1000)
60
+
61
+ expect {
62
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
63
+ }.not_to raise_error
64
+ end
65
+
66
+ it 'raises error when file does not exist' do
67
+ non_existent = 'non_existent.png'
68
+
69
+ expect {
70
+ consolidator.consolidate([spritesheet1, non_existent], output_file)
71
+ }.to raise_error(RubySpriter::ValidationError, /not found/)
72
+ end
73
+ end
74
+
75
+ context 'with metadata reading' do
76
+ it 'reads metadata from all files' do
77
+ stub_consolidation_success
78
+ metadata1 = { columns: 2, rows: 2, frames: 4 }
79
+ metadata2 = { columns: 2, rows: 3, frames: 6 }
80
+
81
+ expect(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(metadata1)
82
+ expect(RubySpriter::MetadataManager).to receive(:read).with(spritesheet2).and_return(metadata2)
83
+
84
+ allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
85
+ allow(File).to receive(:size).and_return(1000)
86
+
87
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
88
+ end
89
+
90
+ it 'raises ValidationError when file missing metadata' do
91
+ allow(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(nil)
92
+
93
+ expect {
94
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
95
+ }.to raise_error(RubySpriter::ValidationError, /missing metadata/)
96
+ end
97
+
98
+ it 'raises error with helpful message about Ruby Spriter spritesheets' do
99
+ allow(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(nil)
100
+
101
+ expect {
102
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
103
+ }.to raise_error(RubySpriter::ValidationError, /All files must be Ruby Spriter spritesheets/)
104
+ end
105
+ end
106
+
107
+ context 'with column validation enabled (default)' do
108
+ let(:consolidator) { described_class.new(validate_columns: true) }
109
+
110
+ it 'passes when all files have same column count' do
111
+ stub_consolidation_success
112
+ metadata1 = { columns: 2, rows: 2, frames: 4 }
113
+ metadata2 = { columns: 2, rows: 3, frames: 6 }
114
+
115
+ allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
116
+ allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
117
+ allow(File).to receive(:size).and_return(1000)
118
+
119
+ expect {
120
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
121
+ }.not_to raise_error
122
+ end
123
+
124
+ it 'raises error when column counts do not match' do
125
+ metadata1 = { columns: 2, rows: 2, frames: 4 }
126
+ metadata2 = { columns: 4, rows: 1, frames: 4 }
127
+
128
+ allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
129
+
130
+ expect {
131
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
132
+ }.to raise_error(RubySpriter::ValidationError, /Column count mismatch/)
133
+ end
134
+
135
+ it 'error message shows expected and actual column counts' do
136
+ metadata1 = { columns: 2, rows: 2, frames: 4 }
137
+ metadata2 = { columns: 4, rows: 1, frames: 4 }
138
+
139
+ allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
140
+
141
+ expect {
142
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
143
+ }.to raise_error(RubySpriter::ValidationError, /Expected 2, found 4/)
144
+ end
145
+
146
+ it 'error message suggests --no-validate-columns flag' do
147
+ metadata1 = { columns: 2, rows: 2, frames: 4 }
148
+ metadata2 = { columns: 4, rows: 1, frames: 4 }
149
+
150
+ allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
151
+
152
+ expect {
153
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
154
+ }.to raise_error(RubySpriter::ValidationError, /--no-validate-columns/)
155
+ end
156
+ end
157
+
158
+ context 'with column validation disabled' do
159
+ let(:consolidator) { described_class.new(validate_columns: false) }
160
+ before { stub_consolidation_success }
161
+
162
+ it 'allows mismatched column counts' do
163
+ metadata1 = { columns: 2, rows: 2, frames: 4 }
164
+ metadata2 = { columns: 4, rows: 1, frames: 4 }
165
+
166
+ allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
167
+ allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
168
+ allow(File).to receive(:size).and_return(1000)
169
+
170
+ expect {
171
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
172
+ }.not_to raise_error
173
+ end
174
+ end
175
+
176
+ context 'with ImageMagick consolidation' do
177
+ before do
178
+ stub_consolidation_success
179
+ allow(RubySpriter::MetadataManager).to receive(:read).and_return(
180
+ { columns: 2, rows: 2, frames: 4 }
181
+ )
182
+ allow(File).to receive(:size).and_return(1000)
183
+ end
184
+
185
+ it 'calls ImageMagick with correct command' do
186
+ expect(Open3).to receive(:capture3) do |cmd|
187
+ expect(cmd).to include('convert')
188
+ expect(cmd).to include('-append')
189
+ expect(cmd).to include(spritesheet1)
190
+ expect(cmd).to include(spritesheet2)
191
+ expect(cmd).to include(output_file)
192
+ ['', '', instance_double(Process::Status, success?: true)]
193
+ end
194
+
195
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
196
+ end
197
+
198
+ it 'uses -append flag for vertical stacking' do
199
+ expect(Open3).to receive(:capture3) do |cmd|
200
+ expect(cmd).to include('-append')
201
+ ['', '', instance_double(Process::Status, success?: true)]
202
+ end
203
+
204
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
205
+ end
206
+
207
+ it 'raises ProcessingError when ImageMagick fails' do
208
+ allow(Open3).to receive(:capture3).and_return(
209
+ ['', 'ImageMagick error', instance_double(Process::Status, success?: false)]
210
+ )
211
+
212
+ expect {
213
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
214
+ }.to raise_error(RubySpriter::ProcessingError, /ImageMagick consolidation failed/)
215
+ end
216
+
217
+ it 'shows debug output when debug option enabled' do
218
+ consolidator = described_class.new(debug: true)
219
+
220
+ allow(RubySpriter::MetadataManager).to receive(:read).and_return(
221
+ { columns: 2, rows: 2, frames: 4 }
222
+ )
223
+ allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
224
+ allow(File).to receive(:size).and_return(1000)
225
+
226
+ expect {
227
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
228
+ }.to output(/DEBUG: ImageMagick command/).to_stdout
229
+ end
230
+ end
231
+
232
+ context 'with successful consolidation' do
233
+ before do
234
+ stub_consolidation_success
235
+ allow(RubySpriter::MetadataManager).to receive(:read).and_return(
236
+ { columns: 2, rows: 2, frames: 4 },
237
+ { columns: 2, rows: 3, frames: 6 }
238
+ )
239
+ allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
240
+ allow(File).to receive(:size).and_return(5000)
241
+ end
242
+
243
+ it 'calculates correct total frames' do
244
+ result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
245
+
246
+ expect(result[:frames]).to eq(10) # 4 + 6
247
+ end
248
+
249
+ it 'calculates correct row count' do
250
+ result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
251
+
252
+ # 10 frames / 2 columns = 5 rows
253
+ expect(result[:rows]).to eq(5)
254
+ end
255
+
256
+ it 'uses columns from first spritesheet' do
257
+ result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
258
+
259
+ expect(result[:columns]).to eq(2)
260
+ end
261
+
262
+ it 'embeds metadata in output file' do
263
+ expect(RubySpriter::MetadataManager).to receive(:embed).with(
264
+ anything,
265
+ output_file,
266
+ hash_including(columns: 2, rows: 5, frames: 10)
267
+ )
268
+
269
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
270
+ end
271
+
272
+ it 'returns hash with correct keys' do
273
+ result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
274
+
275
+ expect(result).to include(
276
+ output_file: output_file,
277
+ columns: 2,
278
+ rows: 5,
279
+ frames: 10,
280
+ size: 5000
281
+ )
282
+ end
283
+
284
+ it 'returns correct file size' do
285
+ result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
286
+
287
+ expect(result[:size]).to eq(5000)
288
+ end
289
+ end
290
+
291
+ context 'with 3 spritesheets' do
292
+ before do
293
+ stub_consolidation_success
294
+ allow(RubySpriter::MetadataManager).to receive(:read).and_return(
295
+ { columns: 4, rows: 1, frames: 4 },
296
+ { columns: 4, rows: 1, frames: 4 },
297
+ { columns: 4, rows: 1, frames: 4 }
298
+ )
299
+ allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
300
+ allow(File).to receive(:size).and_return(8000)
301
+ end
302
+
303
+ it 'successfully consolidates all 3 files' do
304
+ expect(Open3).to receive(:capture3) do |cmd|
305
+ expect(cmd).to include(spritesheet1)
306
+ expect(cmd).to include(spritesheet2)
307
+ expect(cmd).to include(spritesheet3)
308
+ ['', '', instance_double(Process::Status, success?: true)]
309
+ end
310
+
311
+ consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
312
+ end
313
+
314
+ it 'calculates correct total frames for 3 files' do
315
+ result = consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
316
+
317
+ expect(result[:frames]).to eq(12) # 4 + 4 + 4
318
+ end
319
+
320
+ it 'calculates correct row count for 3 files' do
321
+ result = consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
322
+
323
+ # 12 frames / 4 columns = 3 rows
324
+ expect(result[:rows]).to eq(3)
325
+ end
326
+ end
327
+
328
+ context 'with output display' do
329
+ before do
330
+ stub_consolidation_success
331
+ allow(RubySpriter::MetadataManager).to receive(:read).and_return(
332
+ { columns: 2, rows: 2, frames: 4 },
333
+ { columns: 2, rows: 3, frames: 6 }
334
+ )
335
+ allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
336
+ allow(File).to receive(:size).and_return(5000)
337
+ end
338
+
339
+ it 'displays success message' do
340
+ expect {
341
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
342
+ }.to output(/Consolidated spritesheet created/).to_stdout
343
+ end
344
+
345
+ it 'displays output file path' do
346
+ expect {
347
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
348
+ }.to output(/Output:.*consolidated\.png/).to_stdout
349
+ end
350
+
351
+ it 'displays grid layout information' do
352
+ expect {
353
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
354
+ }.to output(/Grid Layout:.*Columns: 2.*Rows: 5.*Total Frames: 10/m).to_stdout
355
+ end
356
+
357
+ it 'displays Godot AnimatedSprite2D settings' do
358
+ expect {
359
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
360
+ }.to output(/Godot AnimatedSprite2D Settings:.*HFrames = 2.*VFrames = 5/m).to_stdout
361
+ end
362
+
363
+ it 'displays source breakdown with frame counts' do
364
+ expect {
365
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
366
+ }.to output(/Source Breakdown:.*spritesheet_4x2\.png.*grid \(4 frames\).*spritesheet_6x2\.png.*grid \(6 frames\)/m).to_stdout
367
+ end
368
+
369
+ it 'displays number of combined spritesheets' do
370
+ expect {
371
+ consolidator.consolidate([spritesheet1, spritesheet2], output_file)
372
+ }.to output(/Combined 2 spritesheets/).to_stdout
373
+ end
374
+ end
375
+ end
376
+
377
+ describe '#find_spritesheets_in_directory' do
378
+ let(:consolidator) { described_class.new }
379
+ let(:image_without_meta) { File.join(__dir__, '..', 'fixtures', 'image_without_metadata.png') }
380
+
381
+ # Helper to create a unique test directory for each test
382
+ def create_test_dir
383
+ dir = File.join($test_temp_dir, "consolidate_test_#{SecureRandom.hex(8)}")
384
+ FileUtils.mkdir_p(dir)
385
+ dir
386
+ end
387
+
388
+ it 'finds all PNG files with metadata in directory' do
389
+ test_dir = create_test_dir
390
+ # Copy spritesheets with metadata to test directory
391
+ sprite1_path = File.join(test_dir, 'sprite1.png')
392
+ sprite2_path = File.join(test_dir, 'sprite2.png')
393
+ FileUtils.cp(spritesheet1, sprite1_path)
394
+ FileUtils.cp(spritesheet2, sprite2_path)
395
+
396
+ # Mock metadata reading to return valid metadata for these files
397
+ allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
398
+ allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
399
+ { columns: 2, rows: 2, frames: 4, version: '0.6' }
400
+ )
401
+ allow(RubySpriter::MetadataManager).to receive(:read).with(sprite2_path).and_return(
402
+ { columns: 2, rows: 3, frames: 6, version: '0.6' }
403
+ )
404
+
405
+ found_files = consolidator.find_spritesheets_in_directory(test_dir)
406
+
407
+ expect(found_files.length).to eq(2)
408
+ expect(found_files).to include(sprite1_path)
409
+ expect(found_files).to include(sprite2_path)
410
+ end
411
+
412
+ it 'excludes PNG files without metadata' do
413
+ test_dir = create_test_dir
414
+ # Copy two spritesheets with metadata and one image without
415
+ sprite1_path = File.join(test_dir, 'sprite1.png')
416
+ sprite2_path = File.join(test_dir, 'sprite2.png')
417
+ no_meta_path = File.join(test_dir, 'no_meta.png')
418
+ FileUtils.cp(spritesheet1, sprite1_path)
419
+ FileUtils.cp(spritesheet2, sprite2_path)
420
+ FileUtils.cp(image_without_meta, no_meta_path)
421
+
422
+ # Mock metadata reading
423
+ allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
424
+ allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
425
+ { columns: 2, rows: 2, frames: 4, version: '0.6' }
426
+ )
427
+ allow(RubySpriter::MetadataManager).to receive(:read).with(sprite2_path).and_return(
428
+ { columns: 2, rows: 3, frames: 6, version: '0.6' }
429
+ )
430
+ allow(RubySpriter::MetadataManager).to receive(:read).with(no_meta_path).and_return(nil)
431
+
432
+ found_files = consolidator.find_spritesheets_in_directory(test_dir)
433
+
434
+ expect(found_files.length).to eq(2)
435
+ expect(found_files).to include(sprite1_path)
436
+ expect(found_files).to include(sprite2_path)
437
+ expect(found_files).not_to include(no_meta_path)
438
+ end
439
+
440
+ it 'returns files sorted alphabetically' do
441
+ test_dir = create_test_dir
442
+ # Copy spritesheets with names that test alphabetical sorting
443
+ c_sprite_path = File.join(test_dir, 'c_sprite.png')
444
+ a_sprite_path = File.join(test_dir, 'a_sprite.png')
445
+ b_sprite_path = File.join(test_dir, 'b_sprite.png')
446
+ FileUtils.cp(spritesheet1, c_sprite_path)
447
+ FileUtils.cp(spritesheet2, a_sprite_path)
448
+ FileUtils.cp(spritesheet3, b_sprite_path)
449
+
450
+ # Mock metadata reading
451
+ allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
452
+ allow(RubySpriter::MetadataManager).to receive(:read).with(c_sprite_path).and_return(
453
+ { columns: 4, rows: 1, frames: 4, version: '0.6' }
454
+ )
455
+ allow(RubySpriter::MetadataManager).to receive(:read).with(a_sprite_path).and_return(
456
+ { columns: 2, rows: 3, frames: 6, version: '0.6' }
457
+ )
458
+ allow(RubySpriter::MetadataManager).to receive(:read).with(b_sprite_path).and_return(
459
+ { columns: 4, rows: 4, frames: 16, version: '0.6' }
460
+ )
461
+
462
+ found_files = consolidator.find_spritesheets_in_directory(test_dir)
463
+
464
+ expect(found_files.length).to eq(3)
465
+ expect(found_files[0]).to end_with('a_sprite.png')
466
+ expect(found_files[1]).to end_with('b_sprite.png')
467
+ expect(found_files[2]).to end_with('c_sprite.png')
468
+ end
469
+
470
+ it 'raises error when directory does not exist' do
471
+ expect {
472
+ consolidator.find_spritesheets_in_directory('nonexistent_directory')
473
+ }.to raise_error(RubySpriter::ValidationError, /Directory not found/)
474
+ end
475
+
476
+ it 'raises error when no PNG files with metadata found' do
477
+ # Create empty directory
478
+ empty_dir = File.join($test_temp_dir, 'empty_dir')
479
+ FileUtils.mkdir_p(empty_dir)
480
+
481
+ expect {
482
+ consolidator.find_spritesheets_in_directory(empty_dir)
483
+ }.to raise_error(RubySpriter::ValidationError, /No PNG files with spritesheet metadata found/)
484
+ end
485
+
486
+ it 'raises error when directory has PNGs but none with metadata' do
487
+ test_dir = create_test_dir
488
+ # Copy only image without metadata
489
+ FileUtils.cp(image_without_meta, File.join(test_dir, 'no_meta.png'))
490
+
491
+ expect {
492
+ consolidator.find_spritesheets_in_directory(test_dir)
493
+ }.to raise_error(RubySpriter::ValidationError, /No PNG files with spritesheet metadata found/)
494
+ end
495
+
496
+ it 'handles directory with mixed file types' do
497
+ test_dir = create_test_dir
498
+ # Copy spritesheets and create non-PNG files
499
+ sprite1_path = File.join(test_dir, 'sprite1.png')
500
+ sprite2_path = File.join(test_dir, 'sprite2.png')
501
+ FileUtils.cp(spritesheet1, sprite1_path)
502
+ FileUtils.cp(spritesheet2, sprite2_path)
503
+ File.write(File.join(test_dir, 'readme.txt'), 'test')
504
+ File.write(File.join(test_dir, 'data.json'), '{}')
505
+
506
+ # Mock metadata reading
507
+ allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
508
+ allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
509
+ { columns: 2, rows: 2, frames: 4, version: '0.6' }
510
+ )
511
+ allow(RubySpriter::MetadataManager).to receive(:read).with(sprite2_path).and_return(
512
+ { columns: 2, rows: 3, frames: 6, version: '0.6' }
513
+ )
514
+
515
+ found_files = consolidator.find_spritesheets_in_directory(test_dir)
516
+
517
+ expect(found_files.length).to eq(2)
518
+ expect(found_files).to all(end_with('.png'))
519
+ end
520
+
521
+ it 'requires at least 2 spritesheets' do
522
+ test_dir = create_test_dir
523
+ # Copy only one spritesheet
524
+ sprite1_path = File.join(test_dir, 'sprite1.png')
525
+ FileUtils.cp(spritesheet1, sprite1_path)
526
+
527
+ # Mock metadata reading
528
+ allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
529
+ allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
530
+ { columns: 2, rows: 2, frames: 4, version: '0.6' }
531
+ )
532
+
533
+ expect {
534
+ consolidator.find_spritesheets_in_directory(test_dir)
535
+ }.to raise_error(RubySpriter::ValidationError, /Found only 1 spritesheet, need at least 2/)
536
+ end
537
+ end
538
+ end