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,1142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubySpriter::CLI do
6
+ describe 'Other Options' do
7
+ describe '--keep-temp flag' do
8
+ it 'sets keep_temp option to true' do
9
+ # Mock the Processor to capture the options
10
+ processor_double = instance_double(RubySpriter::Processor)
11
+ allow(processor_double).to receive(:run)
12
+
13
+ allow(RubySpriter::Processor).to receive(:new) do |options|
14
+ expect(options[:keep_temp]).to eq(true)
15
+ processor_double
16
+ end
17
+
18
+ # Parse with --keep-temp and a valid input to avoid validation errors
19
+ described_class.start(['--video', 'test.mp4', '--keep-temp'])
20
+ end
21
+ end
22
+
23
+ describe '--debug flag' do
24
+ it 'sets both debug and keep_temp options to true' do
25
+ processor_double = instance_double(RubySpriter::Processor)
26
+ allow(processor_double).to receive(:run)
27
+
28
+ allow(RubySpriter::Processor).to receive(:new) do |options|
29
+ expect(options[:debug]).to eq(true)
30
+ expect(options[:keep_temp]).to eq(true)
31
+ processor_double
32
+ end
33
+
34
+ described_class.start(['--video', 'test.mp4', '--debug'])
35
+ end
36
+ end
37
+
38
+ describe '--help flag' do
39
+ it 'outputs help text and exits' do
40
+ expect do
41
+ expect { described_class.start(['--help']) }.to output(/Usage: ruby_spriter/).to_stdout
42
+ end.to raise_error(SystemExit)
43
+ end
44
+
45
+ it 'displays Other Options section' do
46
+ expect do
47
+ expect { described_class.start(['--help']) }.to output(/Other Options:/).to_stdout
48
+ end.to raise_error(SystemExit)
49
+ end
50
+
51
+ it 'lists --keep-temp in help output' do
52
+ expect do
53
+ expect { described_class.start(['--help']) }.to output(/--keep-temp/).to_stdout
54
+ end.to raise_error(SystemExit)
55
+ end
56
+
57
+ it 'lists --debug in help output' do
58
+ expect do
59
+ expect { described_class.start(['--help']) }.to output(/--debug/).to_stdout
60
+ end.to raise_error(SystemExit)
61
+ end
62
+
63
+ it 'lists --version in help output' do
64
+ expect do
65
+ expect { described_class.start(['--help']) }.to output(/--version/).to_stdout
66
+ end.to raise_error(SystemExit)
67
+ end
68
+
69
+ it 'lists --check-dependencies in help output' do
70
+ expect do
71
+ expect { described_class.start(['--help']) }.to output(/--check-dependencies/).to_stdout
72
+ end.to raise_error(SystemExit)
73
+ end
74
+
75
+ it 'supports short form -h' do
76
+ expect do
77
+ expect { described_class.start(['-h']) }.to output(/Usage: ruby_spriter/).to_stdout
78
+ end.to raise_error(SystemExit)
79
+ end
80
+ end
81
+
82
+ describe '--version flag' do
83
+ it 'outputs version information and exits' do
84
+ expect do
85
+ expect { described_class.start(['--version']) }
86
+ .to output(/Ruby Spriter v#{RubySpriter::VERSION}/).to_stdout
87
+ end.to raise_error(SystemExit)
88
+ end
89
+
90
+ it 'displays platform information' do
91
+ expect do
92
+ expect { described_class.start(['--version']) }
93
+ .to output(/Platform:/).to_stdout
94
+ end.to raise_error(SystemExit)
95
+ end
96
+
97
+ it 'displays date information' do
98
+ expect do
99
+ expect { described_class.start(['--version']) }
100
+ .to output(/Date: #{RubySpriter::VERSION_DATE}/).to_stdout
101
+ end.to raise_error(SystemExit)
102
+ end
103
+ end
104
+
105
+ describe '--check-dependencies flag' do
106
+ it 'sets check_dependencies option to true' do
107
+ # Mock DependencyChecker to avoid actually checking dependencies
108
+ checker_double = instance_double(RubySpriter::DependencyChecker)
109
+ allow(checker_double).to receive(:print_report)
110
+ allow(checker_double).to receive(:all_satisfied?).and_return(true)
111
+ allow(RubySpriter::DependencyChecker).to receive(:new).and_return(checker_double)
112
+
113
+ expect do
114
+ described_class.start(['--check-dependencies'])
115
+ end.to raise_error(SystemExit) { |error|
116
+ expect(error.status).to eq(0)
117
+ }
118
+ end
119
+
120
+ it 'exits with 0 when all dependencies are satisfied' do
121
+ checker_double = instance_double(RubySpriter::DependencyChecker)
122
+ allow(checker_double).to receive(:print_report)
123
+ allow(checker_double).to receive(:all_satisfied?).and_return(true)
124
+ allow(RubySpriter::DependencyChecker).to receive(:new).and_return(checker_double)
125
+
126
+ expect do
127
+ described_class.start(['--check-dependencies'])
128
+ end.to raise_error(SystemExit) { |error|
129
+ expect(error.status).to eq(0)
130
+ }
131
+ end
132
+
133
+ it 'exits with 1 when dependencies are missing' do
134
+ checker_double = instance_double(RubySpriter::DependencyChecker)
135
+ allow(checker_double).to receive(:print_report)
136
+ allow(checker_double).to receive(:all_satisfied?).and_return(false)
137
+ allow(RubySpriter::DependencyChecker).to receive(:new).and_return(checker_double)
138
+
139
+ expect do
140
+ described_class.start(['--check-dependencies'])
141
+ end.to raise_error(SystemExit) { |error|
142
+ expect(error.status).to eq(1)
143
+ }
144
+ end
145
+
146
+ it 'calls DependencyChecker with verbose: true' do
147
+ checker_double = instance_double(RubySpriter::DependencyChecker)
148
+ allow(checker_double).to receive(:print_report)
149
+ allow(checker_double).to receive(:all_satisfied?).and_return(true)
150
+
151
+ expect(RubySpriter::DependencyChecker).to receive(:new).with(verbose: true).and_return(checker_double)
152
+
153
+ expect do
154
+ described_class.start(['--check-dependencies'])
155
+ end.to raise_error(SystemExit)
156
+ end
157
+ end
158
+ end
159
+
160
+ describe '--image flag' do
161
+ let(:fixture_with_meta) { File.join(__dir__, '..', 'fixtures', 'spritesheet_with_metadata.png') }
162
+ let(:fixture_without_meta) { File.join(__dir__, '..', 'fixtures', 'image_without_metadata.png') }
163
+
164
+ describe 'argument parsing' do
165
+ it 'sets image option with --image flag' do
166
+ processor_double = instance_double(RubySpriter::Processor)
167
+ allow(processor_double).to receive(:run)
168
+
169
+ allow(RubySpriter::Processor).to receive(:new) do |options|
170
+ expect(options[:image]).to eq(fixture_with_meta)
171
+ processor_double
172
+ end
173
+
174
+ described_class.start(['--image', fixture_with_meta])
175
+ end
176
+
177
+ it 'supports short form -i flag' do
178
+ processor_double = instance_double(RubySpriter::Processor)
179
+ allow(processor_double).to receive(:run)
180
+
181
+ allow(RubySpriter::Processor).to receive(:new) do |options|
182
+ expect(options[:image]).to eq(fixture_without_meta)
183
+ processor_double
184
+ end
185
+
186
+ described_class.start(['-i', fixture_without_meta])
187
+ end
188
+
189
+ it 'accepts file path with spaces' do
190
+ # Create a temp file with spaces in the name for this test
191
+ temp_file = File.join(@test_dir, 'file with spaces.png')
192
+ FileUtils.cp(fixture_with_meta, temp_file)
193
+
194
+ processor_double = instance_double(RubySpriter::Processor)
195
+ allow(processor_double).to receive(:run)
196
+
197
+ allow(RubySpriter::Processor).to receive(:new) do |options|
198
+ expect(options[:image]).to eq(temp_file)
199
+ processor_double
200
+ end
201
+
202
+ described_class.start(['--image', temp_file])
203
+ end
204
+ end
205
+
206
+ describe 'mutual exclusivity with other input modes' do
207
+ it 'cannot be used with --video' do
208
+ expect do
209
+ described_class.start(['--video', 'test.mp4', '--image', fixture_with_meta])
210
+ end.to raise_error(RubySpriter::ValidationError, /Cannot use multiple input modes/)
211
+ end
212
+
213
+ it 'cannot be used with --consolidate' do
214
+ expect do
215
+ described_class.start(['--consolidate', 'a.png,b.png', '--image', fixture_with_meta])
216
+ end.to raise_error(RubySpriter::ValidationError, /Cannot use multiple input modes/)
217
+ end
218
+
219
+ it 'cannot be used with --verify' do
220
+ expect do
221
+ described_class.start(['--verify', fixture_with_meta, '--image', fixture_without_meta])
222
+ end.to raise_error(RubySpriter::ValidationError, /Cannot use multiple input modes/)
223
+ end
224
+
225
+ it 'can be used alone without error' do
226
+ processor_double = instance_double(RubySpriter::Processor)
227
+ allow(processor_double).to receive(:run)
228
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
229
+
230
+ expect do
231
+ described_class.start(['--image', fixture_with_meta])
232
+ end.not_to raise_error
233
+ end
234
+ end
235
+
236
+ describe 'file validation' do
237
+ describe 'file existence' do
238
+ it 'raises error for non-existent file' do
239
+ expect do
240
+ described_class.start(['--image', 'nonexistent.png'])
241
+ end.to raise_error(RubySpriter::ValidationError, /File not found/)
242
+ end
243
+
244
+ it 'accepts existing PNG file with metadata' do
245
+ processor_double = instance_double(RubySpriter::Processor)
246
+ allow(processor_double).to receive(:run)
247
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
248
+
249
+ expect(File.exist?(fixture_with_meta)).to be true
250
+ expect do
251
+ described_class.start(['--image', fixture_with_meta])
252
+ end.not_to raise_error
253
+ end
254
+
255
+ it 'accepts existing PNG file without metadata' do
256
+ processor_double = instance_double(RubySpriter::Processor)
257
+ allow(processor_double).to receive(:run)
258
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
259
+
260
+ expect(File.exist?(fixture_without_meta)).to be true
261
+ expect do
262
+ described_class.start(['--image', fixture_without_meta])
263
+ end.not_to raise_error
264
+ end
265
+ end
266
+
267
+ describe 'file extension validation' do
268
+ it 'accepts .png extension' do
269
+ processor_double = instance_double(RubySpriter::Processor)
270
+ allow(processor_double).to receive(:run)
271
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
272
+
273
+ expect(File.extname(fixture_with_meta)).to eq('.png')
274
+ expect do
275
+ described_class.start(['--image', fixture_with_meta])
276
+ end.not_to raise_error
277
+ end
278
+
279
+ it 'accepts .PNG extension (case insensitive)' do
280
+ # Create a temp file with uppercase extension
281
+ temp_file = File.join(@test_dir, 'test.PNG')
282
+ FileUtils.cp(fixture_with_meta, temp_file)
283
+
284
+ processor_double = instance_double(RubySpriter::Processor)
285
+ allow(processor_double).to receive(:run)
286
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
287
+
288
+ expect do
289
+ described_class.start(['--image', temp_file])
290
+ end.not_to raise_error
291
+ end
292
+
293
+ it 'rejects .jpg extension' do
294
+ # Create a fake .jpg file (doesn't need to be valid JPG for this test)
295
+ temp_file = File.join(@test_dir, 'test.jpg')
296
+ FileUtils.touch(temp_file)
297
+
298
+ expect do
299
+ described_class.start(['--image', temp_file])
300
+ end.to raise_error(RubySpriter::ValidationError, /--image expects \.png file, got: \.jpg/)
301
+ end
302
+
303
+ it 'rejects .jpeg extension' do
304
+ temp_file = File.join(@test_dir, 'test.jpeg')
305
+ FileUtils.touch(temp_file)
306
+
307
+ expect do
308
+ described_class.start(['--image', temp_file])
309
+ end.to raise_error(RubySpriter::ValidationError, /--image expects \.png file, got: \.jpeg/)
310
+ end
311
+
312
+ it 'rejects .gif extension' do
313
+ temp_file = File.join(@test_dir, 'test.gif')
314
+ FileUtils.touch(temp_file)
315
+
316
+ expect do
317
+ described_class.start(['--image', temp_file])
318
+ end.to raise_error(RubySpriter::ValidationError, /--image expects \.png file, got: \.gif/)
319
+ end
320
+
321
+ it 'rejects .bmp extension' do
322
+ temp_file = File.join(@test_dir, 'test.bmp')
323
+ FileUtils.touch(temp_file)
324
+
325
+ expect do
326
+ described_class.start(['--image', temp_file])
327
+ end.to raise_error(RubySpriter::ValidationError, /--image expects \.png file, got: \.bmp/)
328
+ end
329
+
330
+ it 'rejects file with no extension' do
331
+ temp_file = File.join(@test_dir, 'testfile')
332
+ FileUtils.touch(temp_file)
333
+
334
+ expect do
335
+ described_class.start(['--image', temp_file])
336
+ end.to raise_error(RubySpriter::ValidationError, /--image expects \.png file/)
337
+ end
338
+ end
339
+ end
340
+
341
+ describe 'integration with processing options' do
342
+ it 'works with --scale option' do
343
+ processor_double = instance_double(RubySpriter::Processor)
344
+ allow(processor_double).to receive(:run)
345
+
346
+ allow(RubySpriter::Processor).to receive(:new) do |options|
347
+ expect(options[:image]).to eq(fixture_with_meta)
348
+ expect(options[:scale_percent]).to eq(50)
349
+ processor_double
350
+ end
351
+
352
+ described_class.start(['--image', fixture_with_meta, '--scale', '50'])
353
+ end
354
+
355
+ it 'works with --remove-bg option' do
356
+ processor_double = instance_double(RubySpriter::Processor)
357
+ allow(processor_double).to receive(:run)
358
+
359
+ allow(RubySpriter::Processor).to receive(:new) do |options|
360
+ expect(options[:image]).to eq(fixture_with_meta)
361
+ expect(options[:remove_bg]).to eq(true)
362
+ processor_double
363
+ end
364
+
365
+ described_class.start(['--image', fixture_with_meta, '--remove-bg'])
366
+ end
367
+
368
+ it 'works with --sharpen option' do
369
+ processor_double = instance_double(RubySpriter::Processor)
370
+ allow(processor_double).to receive(:run)
371
+
372
+ allow(RubySpriter::Processor).to receive(:new) do |options|
373
+ expect(options[:image]).to eq(fixture_without_meta)
374
+ expect(options[:sharpen]).to eq(true)
375
+ processor_double
376
+ end
377
+
378
+ described_class.start(['--image', fixture_without_meta, '--sharpen'])
379
+ end
380
+
381
+ it 'works with --interpolation option' do
382
+ processor_double = instance_double(RubySpriter::Processor)
383
+ allow(processor_double).to receive(:run)
384
+
385
+ allow(RubySpriter::Processor).to receive(:new) do |options|
386
+ expect(options[:image]).to eq(fixture_with_meta)
387
+ expect(options[:scale_interpolation]).to eq('nohalo')
388
+ processor_double
389
+ end
390
+
391
+ described_class.start(['--image', fixture_with_meta, '--interpolation', 'nohalo'])
392
+ end
393
+
394
+ it 'works with multiple processing options combined' do
395
+ processor_double = instance_double(RubySpriter::Processor)
396
+ allow(processor_double).to receive(:run)
397
+
398
+ allow(RubySpriter::Processor).to receive(:new) do |options|
399
+ expect(options[:image]).to eq(fixture_without_meta)
400
+ expect(options[:scale_percent]).to eq(50)
401
+ expect(options[:remove_bg]).to eq(true)
402
+ expect(options[:sharpen]).to eq(true)
403
+ expect(options[:scale_interpolation]).to eq('lohalo')
404
+ processor_double
405
+ end
406
+
407
+ described_class.start([
408
+ '--image', fixture_without_meta,
409
+ '--scale', '50',
410
+ '--remove-bg',
411
+ '--sharpen',
412
+ '--interpolation', 'lohalo'
413
+ ])
414
+ end
415
+
416
+ it 'works with --output option' do
417
+ processor_double = instance_double(RubySpriter::Processor)
418
+ allow(processor_double).to receive(:run)
419
+
420
+ allow(RubySpriter::Processor).to receive(:new) do |options|
421
+ expect(options[:image]).to eq(fixture_with_meta)
422
+ expect(options[:output]).to eq('custom_output.png')
423
+ processor_double
424
+ end
425
+
426
+ described_class.start(['--image', fixture_with_meta, '--output', 'custom_output.png'])
427
+ end
428
+ end
429
+ end
430
+
431
+ describe '--video flag' do
432
+ let(:fixture_video) { File.join(__dir__, '..', 'fixtures', 'test_video.mp4') }
433
+
434
+ describe 'argument parsing' do
435
+ it 'sets video option with --video flag' do
436
+ processor_double = instance_double(RubySpriter::Processor)
437
+ allow(processor_double).to receive(:run)
438
+
439
+ allow(RubySpriter::Processor).to receive(:new) do |options|
440
+ expect(options[:video]).to eq(fixture_video)
441
+ processor_double
442
+ end
443
+
444
+ described_class.start(['--video', fixture_video])
445
+ end
446
+
447
+ it 'supports short form -v flag' do
448
+ processor_double = instance_double(RubySpriter::Processor)
449
+ allow(processor_double).to receive(:run)
450
+
451
+ allow(RubySpriter::Processor).to receive(:new) do |options|
452
+ expect(options[:video]).to eq(fixture_video)
453
+ processor_double
454
+ end
455
+
456
+ described_class.start(['-v', fixture_video])
457
+ end
458
+
459
+ it 'accepts file path with spaces' do
460
+ # Create a temp file with spaces in the name for this test
461
+ temp_file = File.join(@test_dir, 'video with spaces.mp4')
462
+ FileUtils.cp(fixture_video, temp_file)
463
+
464
+ processor_double = instance_double(RubySpriter::Processor)
465
+ allow(processor_double).to receive(:run)
466
+
467
+ allow(RubySpriter::Processor).to receive(:new) do |options|
468
+ expect(options[:video]).to eq(temp_file)
469
+ processor_double
470
+ end
471
+
472
+ described_class.start(['--video', temp_file])
473
+ end
474
+ end
475
+
476
+ describe 'mutual exclusivity with other input modes' do
477
+ it 'cannot be used with --image' do
478
+ expect do
479
+ described_class.start(['--video', fixture_video, '--image', 'test.png'])
480
+ end.to raise_error(RubySpriter::ValidationError, /Cannot use multiple input modes/)
481
+ end
482
+
483
+ it 'cannot be used with --consolidate' do
484
+ expect do
485
+ described_class.start(['--video', fixture_video, '--consolidate', 'a.png,b.png'])
486
+ end.to raise_error(RubySpriter::ValidationError, /Cannot use multiple input modes/)
487
+ end
488
+
489
+ it 'cannot be used with --verify' do
490
+ expect do
491
+ described_class.start(['--video', fixture_video, '--verify', 'test.png'])
492
+ end.to raise_error(RubySpriter::ValidationError, /Cannot use multiple input modes/)
493
+ end
494
+
495
+ it 'can be used alone without error' do
496
+ processor_double = instance_double(RubySpriter::Processor)
497
+ allow(processor_double).to receive(:run)
498
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
499
+
500
+ expect do
501
+ described_class.start(['--video', fixture_video])
502
+ end.not_to raise_error
503
+ end
504
+ end
505
+
506
+ describe 'file validation' do
507
+ describe 'file existence' do
508
+ it 'raises error for non-existent file' do
509
+ expect do
510
+ described_class.start(['--video', 'nonexistent.mp4'])
511
+ end.to raise_error(RubySpriter::ValidationError, /File not found/)
512
+ end
513
+
514
+ it 'accepts existing MP4 file' do
515
+ processor_double = instance_double(RubySpriter::Processor)
516
+ allow(processor_double).to receive(:run)
517
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
518
+
519
+ expect(File.exist?(fixture_video)).to be true
520
+ expect do
521
+ described_class.start(['--video', fixture_video])
522
+ end.not_to raise_error
523
+ end
524
+ end
525
+
526
+ describe 'file extension validation' do
527
+ it 'accepts .mp4 extension' do
528
+ processor_double = instance_double(RubySpriter::Processor)
529
+ allow(processor_double).to receive(:run)
530
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
531
+
532
+ expect(File.extname(fixture_video)).to eq('.mp4')
533
+ expect do
534
+ described_class.start(['--video', fixture_video])
535
+ end.not_to raise_error
536
+ end
537
+
538
+ it 'accepts .MP4 extension (case insensitive)' do
539
+ # Create a temp file with uppercase extension
540
+ temp_file = File.join(@test_dir, 'test.MP4')
541
+ FileUtils.cp(fixture_video, temp_file)
542
+
543
+ processor_double = instance_double(RubySpriter::Processor)
544
+ allow(processor_double).to receive(:run)
545
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
546
+
547
+ expect do
548
+ described_class.start(['--video', temp_file])
549
+ end.not_to raise_error
550
+ end
551
+
552
+ it 'rejects .avi extension' do
553
+ temp_file = File.join(@test_dir, 'test.avi')
554
+ FileUtils.touch(temp_file)
555
+
556
+ expect do
557
+ described_class.start(['--video', temp_file])
558
+ end.to raise_error(RubySpriter::ValidationError, /--video expects \.mp4 file, got: \.avi/)
559
+ end
560
+
561
+ it 'rejects .mov extension' do
562
+ temp_file = File.join(@test_dir, 'test.mov')
563
+ FileUtils.touch(temp_file)
564
+
565
+ expect do
566
+ described_class.start(['--video', temp_file])
567
+ end.to raise_error(RubySpriter::ValidationError, /--video expects \.mp4 file, got: \.mov/)
568
+ end
569
+
570
+ it 'rejects .mkv extension' do
571
+ temp_file = File.join(@test_dir, 'test.mkv')
572
+ FileUtils.touch(temp_file)
573
+
574
+ expect do
575
+ described_class.start(['--video', temp_file])
576
+ end.to raise_error(RubySpriter::ValidationError, /--video expects \.mp4 file, got: \.mkv/)
577
+ end
578
+
579
+ it 'rejects .wmv extension' do
580
+ temp_file = File.join(@test_dir, 'test.wmv')
581
+ FileUtils.touch(temp_file)
582
+
583
+ expect do
584
+ described_class.start(['--video', temp_file])
585
+ end.to raise_error(RubySpriter::ValidationError, /--video expects \.mp4 file, got: \.wmv/)
586
+ end
587
+
588
+ it 'rejects file with no extension' do
589
+ temp_file = File.join(@test_dir, 'videofile')
590
+ FileUtils.touch(temp_file)
591
+
592
+ expect do
593
+ described_class.start(['--video', temp_file])
594
+ end.to raise_error(RubySpriter::ValidationError, /--video expects \.mp4 file/)
595
+ end
596
+ end
597
+ end
598
+
599
+ describe 'integration with video-specific options' do
600
+ it 'works with --frames option' do
601
+ processor_double = instance_double(RubySpriter::Processor)
602
+ allow(processor_double).to receive(:run)
603
+
604
+ allow(RubySpriter::Processor).to receive(:new) do |options|
605
+ expect(options[:video]).to eq(fixture_video)
606
+ expect(options[:frame_count]).to eq(32)
607
+ processor_double
608
+ end
609
+
610
+ described_class.start(['--video', fixture_video, '--frames', '32'])
611
+ end
612
+
613
+ it 'works with --columns option' do
614
+ processor_double = instance_double(RubySpriter::Processor)
615
+ allow(processor_double).to receive(:run)
616
+
617
+ allow(RubySpriter::Processor).to receive(:new) do |options|
618
+ expect(options[:video]).to eq(fixture_video)
619
+ expect(options[:columns]).to eq(8)
620
+ processor_double
621
+ end
622
+
623
+ described_class.start(['--video', fixture_video, '--columns', '8'])
624
+ end
625
+
626
+ it 'works with --width option' do
627
+ processor_double = instance_double(RubySpriter::Processor)
628
+ allow(processor_double).to receive(:run)
629
+
630
+ allow(RubySpriter::Processor).to receive(:new) do |options|
631
+ expect(options[:video]).to eq(fixture_video)
632
+ expect(options[:max_width]).to eq(640)
633
+ processor_double
634
+ end
635
+
636
+ described_class.start(['--video', fixture_video, '--width', '640'])
637
+ end
638
+
639
+ it 'works with --background option' do
640
+ processor_double = instance_double(RubySpriter::Processor)
641
+ allow(processor_double).to receive(:run)
642
+
643
+ allow(RubySpriter::Processor).to receive(:new) do |options|
644
+ expect(options[:video]).to eq(fixture_video)
645
+ expect(options[:bg_color]).to eq('white')
646
+ processor_double
647
+ end
648
+
649
+ described_class.start(['--video', fixture_video, '--background', 'white'])
650
+ end
651
+
652
+ it 'works with multiple video options combined' do
653
+ processor_double = instance_double(RubySpriter::Processor)
654
+ allow(processor_double).to receive(:run)
655
+
656
+ allow(RubySpriter::Processor).to receive(:new) do |options|
657
+ expect(options[:video]).to eq(fixture_video)
658
+ expect(options[:frame_count]).to eq(64)
659
+ expect(options[:columns]).to eq(8)
660
+ expect(options[:max_width]).to eq(480)
661
+ expect(options[:bg_color]).to eq('white')
662
+ processor_double
663
+ end
664
+
665
+ described_class.start([
666
+ '--video', fixture_video,
667
+ '--frames', '64',
668
+ '--columns', '8',
669
+ '--width', '480',
670
+ '--background', 'white'
671
+ ])
672
+ end
673
+ end
674
+
675
+ describe 'integration with processing options' do
676
+ it 'works with --scale option' do
677
+ processor_double = instance_double(RubySpriter::Processor)
678
+ allow(processor_double).to receive(:run)
679
+
680
+ allow(RubySpriter::Processor).to receive(:new) do |options|
681
+ expect(options[:video]).to eq(fixture_video)
682
+ expect(options[:scale_percent]).to eq(50)
683
+ processor_double
684
+ end
685
+
686
+ described_class.start(['--video', fixture_video, '--scale', '50'])
687
+ end
688
+
689
+ it 'works with --remove-bg option' do
690
+ processor_double = instance_double(RubySpriter::Processor)
691
+ allow(processor_double).to receive(:run)
692
+
693
+ allow(RubySpriter::Processor).to receive(:new) do |options|
694
+ expect(options[:video]).to eq(fixture_video)
695
+ expect(options[:remove_bg]).to eq(true)
696
+ processor_double
697
+ end
698
+
699
+ described_class.start(['--video', fixture_video, '--remove-bg'])
700
+ end
701
+
702
+ it 'works with --sharpen option' do
703
+ processor_double = instance_double(RubySpriter::Processor)
704
+ allow(processor_double).to receive(:run)
705
+
706
+ allow(RubySpriter::Processor).to receive(:new) do |options|
707
+ expect(options[:video]).to eq(fixture_video)
708
+ expect(options[:sharpen]).to eq(true)
709
+ processor_double
710
+ end
711
+
712
+ described_class.start(['--video', fixture_video, '--sharpen'])
713
+ end
714
+
715
+ it 'works with --interpolation option' do
716
+ processor_double = instance_double(RubySpriter::Processor)
717
+ allow(processor_double).to receive(:run)
718
+
719
+ allow(RubySpriter::Processor).to receive(:new) do |options|
720
+ expect(options[:video]).to eq(fixture_video)
721
+ expect(options[:scale_interpolation]).to eq('lohalo')
722
+ processor_double
723
+ end
724
+
725
+ described_class.start(['--video', fixture_video, '--interpolation', 'lohalo'])
726
+ end
727
+
728
+ it 'works with all options combined' do
729
+ processor_double = instance_double(RubySpriter::Processor)
730
+ allow(processor_double).to receive(:run)
731
+
732
+ allow(RubySpriter::Processor).to receive(:new) do |options|
733
+ expect(options[:video]).to eq(fixture_video)
734
+ expect(options[:frame_count]).to eq(32)
735
+ expect(options[:columns]).to eq(8)
736
+ expect(options[:scale_percent]).to eq(50)
737
+ expect(options[:remove_bg]).to eq(true)
738
+ expect(options[:sharpen]).to eq(true)
739
+ expect(options[:scale_interpolation]).to eq('nohalo')
740
+ processor_double
741
+ end
742
+
743
+ described_class.start([
744
+ '--video', fixture_video,
745
+ '--frames', '32',
746
+ '--columns', '8',
747
+ '--scale', '50',
748
+ '--remove-bg',
749
+ '--sharpen',
750
+ '--interpolation', 'nohalo'
751
+ ])
752
+ end
753
+
754
+ it 'works with --output option' do
755
+ processor_double = instance_double(RubySpriter::Processor)
756
+ allow(processor_double).to receive(:run)
757
+
758
+ allow(RubySpriter::Processor).to receive(:new) do |options|
759
+ expect(options[:video]).to eq(fixture_video)
760
+ expect(options[:output]).to eq('custom_spritesheet.png')
761
+ processor_double
762
+ end
763
+
764
+ described_class.start(['--video', fixture_video, '--output', 'custom_spritesheet.png'])
765
+ end
766
+ end
767
+
768
+ describe 'preset configurations' do
769
+ it 'works with --preset thumbnail' do
770
+ processor_double = instance_double(RubySpriter::Processor)
771
+ allow(processor_double).to receive(:run)
772
+
773
+ allow(RubySpriter::Processor).to receive(:new) do |options|
774
+ expect(options[:video]).to eq(fixture_video)
775
+ expect(options[:columns]).to eq(3)
776
+ expect(options[:frame_count]).to eq(9)
777
+ expect(options[:max_width]).to eq(240)
778
+ processor_double
779
+ end
780
+
781
+ described_class.start(['--video', fixture_video, '--preset', 'thumbnail'])
782
+ end
783
+
784
+ it 'works with --preset preview' do
785
+ processor_double = instance_double(RubySpriter::Processor)
786
+ allow(processor_double).to receive(:run)
787
+
788
+ allow(RubySpriter::Processor).to receive(:new) do |options|
789
+ expect(options[:video]).to eq(fixture_video)
790
+ expect(options[:columns]).to eq(4)
791
+ expect(options[:frame_count]).to eq(16)
792
+ expect(options[:max_width]).to eq(400)
793
+ processor_double
794
+ end
795
+
796
+ described_class.start(['--video', fixture_video, '--preset', 'preview'])
797
+ end
798
+
799
+ it 'works with --preset detailed' do
800
+ processor_double = instance_double(RubySpriter::Processor)
801
+ allow(processor_double).to receive(:run)
802
+
803
+ allow(RubySpriter::Processor).to receive(:new) do |options|
804
+ expect(options[:video]).to eq(fixture_video)
805
+ expect(options[:columns]).to eq(10)
806
+ expect(options[:frame_count]).to eq(50)
807
+ expect(options[:max_width]).to eq(320)
808
+ processor_double
809
+ end
810
+
811
+ described_class.start(['--video', fixture_video, '--preset', 'detailed'])
812
+ end
813
+
814
+ it 'works with --preset contact' do
815
+ processor_double = instance_double(RubySpriter::Processor)
816
+ allow(processor_double).to receive(:run)
817
+
818
+ allow(RubySpriter::Processor).to receive(:new) do |options|
819
+ expect(options[:video]).to eq(fixture_video)
820
+ expect(options[:columns]).to eq(8)
821
+ expect(options[:frame_count]).to eq(64)
822
+ expect(options[:max_width]).to eq(160)
823
+ processor_double
824
+ end
825
+
826
+ described_class.start(['--video', fixture_video, '--preset', 'contact'])
827
+ end
828
+ end
829
+ end
830
+
831
+ describe '--consolidate flag' do
832
+ # Real spritesheets generated from test_video.mp4 using --video
833
+ # These demonstrate the actual workflow: --video creates spritesheets, --consolidate combines them
834
+ let(:spritesheet_4x2) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x2.png') } # 2 cols, 2 rows, 4 frames
835
+ let(:spritesheet_6x2) { File.join(__dir__, '..', 'fixtures', 'spritesheet_6x2.png') } # 2 cols, 3 rows, 6 frames
836
+ let(:spritesheet_4x4) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x4.png') } # 4 cols, 1 row, 4 frames (different columns)
837
+
838
+ # Generic PNG fixtures for edge case testing
839
+ let(:fixture_with_meta) { File.join(__dir__, '..', 'fixtures', 'spritesheet_with_metadata.png') }
840
+ let(:fixture_without_meta) { File.join(__dir__, '..', 'fixtures', 'image_without_metadata.png') }
841
+
842
+ describe 'argument parsing' do
843
+ it 'accepts comma-separated list of files' do
844
+ processor_double = instance_double(RubySpriter::Processor)
845
+ allow(processor_double).to receive(:run)
846
+
847
+ allow(RubySpriter::Processor).to receive(:new) do |options|
848
+ expect(options[:consolidate]).to eq([spritesheet_4x2, spritesheet_6x2])
849
+ processor_double
850
+ end
851
+
852
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}"])
853
+ end
854
+
855
+ it 'accepts three or more files' do
856
+ processor_double = instance_double(RubySpriter::Processor)
857
+ allow(processor_double).to receive(:run)
858
+
859
+ allow(RubySpriter::Processor).to receive(:new) do |options|
860
+ expect(options[:consolidate]).to eq([spritesheet_4x2, spritesheet_6x2, spritesheet_4x4])
861
+ processor_double
862
+ end
863
+
864
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2},#{spritesheet_4x4}"])
865
+ end
866
+
867
+ it 'accepts file paths with spaces' do
868
+ # Create temp files with spaces in names
869
+ temp_file1 = File.join(@test_dir, 'file with spaces 1.png')
870
+ temp_file2 = File.join(@test_dir, 'file with spaces 2.png')
871
+ FileUtils.cp(spritesheet_4x2, temp_file1)
872
+ FileUtils.cp(spritesheet_6x2, temp_file2)
873
+
874
+ processor_double = instance_double(RubySpriter::Processor)
875
+ allow(processor_double).to receive(:run)
876
+
877
+ allow(RubySpriter::Processor).to receive(:new) do |options|
878
+ expect(options[:consolidate]).to eq([temp_file1, temp_file2])
879
+ processor_double
880
+ end
881
+
882
+ described_class.start(['--consolidate', "#{temp_file1},#{temp_file2}"])
883
+ end
884
+ end
885
+
886
+ describe 'minimum file count validation' do
887
+ it 'requires at least 2 files' do
888
+ expect do
889
+ described_class.start(['--consolidate', spritesheet_4x2])
890
+ end.to raise_error(RubySpriter::ValidationError, /requires at least 2 files/)
891
+ end
892
+
893
+ it 'accepts exactly 2 files' do
894
+ processor_double = instance_double(RubySpriter::Processor)
895
+ allow(processor_double).to receive(:run)
896
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
897
+
898
+ expect do
899
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}"])
900
+ end.not_to raise_error
901
+ end
902
+
903
+ it 'accepts more than 2 files' do
904
+ processor_double = instance_double(RubySpriter::Processor)
905
+ allow(processor_double).to receive(:run)
906
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
907
+
908
+ expect do
909
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2},#{spritesheet_4x4}"])
910
+ end.not_to raise_error
911
+ end
912
+ end
913
+
914
+ describe 'mutual exclusivity with other input modes' do
915
+ it 'cannot be used with --video' do
916
+ expect do
917
+ described_class.start(['--video', 'test.mp4', '--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}"])
918
+ end.to raise_error(RubySpriter::ValidationError, /Cannot use multiple input modes/)
919
+ end
920
+
921
+ it 'cannot be used with --image' do
922
+ expect do
923
+ described_class.start(['--image', spritesheet_4x2, '--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}"])
924
+ end.to raise_error(RubySpriter::ValidationError, /Cannot use multiple input modes/)
925
+ end
926
+
927
+ it 'cannot be used with --verify' do
928
+ expect do
929
+ described_class.start(['--verify', spritesheet_4x2, '--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}"])
930
+ end.to raise_error(RubySpriter::ValidationError, /Cannot use multiple input modes/)
931
+ end
932
+
933
+ it 'can be used alone without error' do
934
+ processor_double = instance_double(RubySpriter::Processor)
935
+ allow(processor_double).to receive(:run)
936
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
937
+
938
+ expect do
939
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}"])
940
+ end.not_to raise_error
941
+ end
942
+ end
943
+
944
+ describe 'file validation' do
945
+ describe 'file existence' do
946
+ it 'raises error if first file does not exist' do
947
+ expect do
948
+ described_class.start(['--consolidate', "nonexistent1.png,#{spritesheet_4x2}"])
949
+ end.to raise_error(RubySpriter::ValidationError, /File not found/)
950
+ end
951
+
952
+ it 'raises error if second file does not exist' do
953
+ expect do
954
+ described_class.start(['--consolidate', "#{spritesheet_4x2},nonexistent2.png"])
955
+ end.to raise_error(RubySpriter::ValidationError, /File not found/)
956
+ end
957
+
958
+ it 'raises error if any file in list does not exist' do
959
+ expect do
960
+ described_class.start(['--consolidate', "#{spritesheet_4x2},nonexistent.png,#{spritesheet_6x2}"])
961
+ end.to raise_error(RubySpriter::ValidationError, /File not found/)
962
+ end
963
+
964
+ it 'accepts all existing spritesheet files' do
965
+ processor_double = instance_double(RubySpriter::Processor)
966
+ allow(processor_double).to receive(:run)
967
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
968
+
969
+ expect(File.exist?(spritesheet_4x2)).to be true
970
+ expect(File.exist?(spritesheet_6x2)).to be true
971
+
972
+ expect do
973
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}"])
974
+ end.not_to raise_error
975
+ end
976
+ end
977
+
978
+ describe 'file extension validation' do
979
+ it 'accepts all .png spritesheet files' do
980
+ processor_double = instance_double(RubySpriter::Processor)
981
+ allow(processor_double).to receive(:run)
982
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
983
+
984
+ expect do
985
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}"])
986
+ end.not_to raise_error
987
+ end
988
+
989
+ it 'accepts .PNG extension (case insensitive)' do
990
+ temp_file1 = File.join(@test_dir, 'test1.PNG')
991
+ temp_file2 = File.join(@test_dir, 'test2.PNG')
992
+ FileUtils.cp(spritesheet_4x2, temp_file1)
993
+ FileUtils.cp(spritesheet_6x2, temp_file2)
994
+
995
+ processor_double = instance_double(RubySpriter::Processor)
996
+ allow(processor_double).to receive(:run)
997
+ allow(RubySpriter::Processor).to receive(:new).and_return(processor_double)
998
+
999
+ expect do
1000
+ described_class.start(['--consolidate', "#{temp_file1},#{temp_file2}"])
1001
+ end.not_to raise_error
1002
+ end
1003
+
1004
+ it 'rejects files with .jpg extension' do
1005
+ temp_file = File.join(@test_dir, 'test.jpg')
1006
+ FileUtils.touch(temp_file)
1007
+
1008
+ expect do
1009
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{temp_file}"])
1010
+ end.to raise_error(RubySpriter::ValidationError, /--consolidate expects \.png file, got: \.jpg/)
1011
+ end
1012
+
1013
+ it 'rejects files with .mp4 extension' do
1014
+ temp_file = File.join(@test_dir, 'test.mp4')
1015
+ FileUtils.touch(temp_file)
1016
+
1017
+ expect do
1018
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{temp_file}"])
1019
+ end.to raise_error(RubySpriter::ValidationError, /--consolidate expects \.png file, got: \.mp4/)
1020
+ end
1021
+
1022
+ it 'rejects files with no extension' do
1023
+ temp_file = File.join(@test_dir, 'noextension')
1024
+ FileUtils.touch(temp_file)
1025
+
1026
+ expect do
1027
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{temp_file}"])
1028
+ end.to raise_error(RubySpriter::ValidationError, /--consolidate expects \.png file/)
1029
+ end
1030
+
1031
+ it 'validates all files in the list' do
1032
+ temp_file1 = File.join(@test_dir, 'test1.jpg')
1033
+ temp_file2 = File.join(@test_dir, 'test2.gif')
1034
+ FileUtils.touch(temp_file1)
1035
+ FileUtils.touch(temp_file2)
1036
+
1037
+ # Should fail on the first non-PNG file
1038
+ expect do
1039
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{temp_file1},#{temp_file2}"])
1040
+ end.to raise_error(RubySpriter::ValidationError, /--consolidate expects \.png file/)
1041
+ end
1042
+ end
1043
+ end
1044
+
1045
+ describe 'consolidation-specific options' do
1046
+ it 'works with --validate-columns flag (default true)' do
1047
+ processor_double = instance_double(RubySpriter::Processor)
1048
+ allow(processor_double).to receive(:run)
1049
+
1050
+ allow(RubySpriter::Processor).to receive(:new) do |options|
1051
+ expect(options[:consolidate]).to eq([spritesheet_4x2, spritesheet_6x2])
1052
+ expect(options[:validate_columns]).to eq(true)
1053
+ processor_double
1054
+ end
1055
+
1056
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}", '--validate-columns'])
1057
+ end
1058
+
1059
+ it 'works with --no-validate-columns flag' do
1060
+ processor_double = instance_double(RubySpriter::Processor)
1061
+ allow(processor_double).to receive(:run)
1062
+
1063
+ allow(RubySpriter::Processor).to receive(:new) do |options|
1064
+ expect(options[:consolidate]).to eq([spritesheet_4x2, spritesheet_6x2])
1065
+ expect(options[:validate_columns]).to eq(false)
1066
+ processor_double
1067
+ end
1068
+
1069
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}", '--no-validate-columns'])
1070
+ end
1071
+ end
1072
+
1073
+ describe 'integration with other options' do
1074
+ it 'works with --output option' do
1075
+ processor_double = instance_double(RubySpriter::Processor)
1076
+ allow(processor_double).to receive(:run)
1077
+
1078
+ allow(RubySpriter::Processor).to receive(:new) do |options|
1079
+ expect(options[:consolidate]).to eq([spritesheet_4x2, spritesheet_6x2])
1080
+ expect(options[:output]).to eq('consolidated_output.png')
1081
+ processor_double
1082
+ end
1083
+
1084
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}", '--output', 'consolidated_output.png'])
1085
+ end
1086
+
1087
+ it 'works with --debug option' do
1088
+ processor_double = instance_double(RubySpriter::Processor)
1089
+ allow(processor_double).to receive(:run)
1090
+
1091
+ allow(RubySpriter::Processor).to receive(:new) do |options|
1092
+ expect(options[:consolidate]).to eq([spritesheet_4x2, spritesheet_6x2])
1093
+ expect(options[:debug]).to eq(true)
1094
+ expect(options[:keep_temp]).to eq(true)
1095
+ processor_double
1096
+ end
1097
+
1098
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}", '--debug'])
1099
+ end
1100
+
1101
+ it 'works with multiple options combined' do
1102
+ processor_double = instance_double(RubySpriter::Processor)
1103
+ allow(processor_double).to receive(:run)
1104
+
1105
+ allow(RubySpriter::Processor).to receive(:new) do |options|
1106
+ expect(options[:consolidate]).to eq([spritesheet_4x2, spritesheet_6x2])
1107
+ expect(options[:validate_columns]).to eq(false)
1108
+ expect(options[:output]).to eq('combined.png')
1109
+ expect(options[:debug]).to eq(true)
1110
+ processor_double
1111
+ end
1112
+
1113
+ described_class.start([
1114
+ '--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}",
1115
+ '--no-validate-columns',
1116
+ '--output', 'combined.png',
1117
+ '--debug'
1118
+ ])
1119
+ end
1120
+ end
1121
+ end
1122
+
1123
+ describe 'error handling' do
1124
+ describe 'invalid option' do
1125
+ it 'displays error message for invalid option' do
1126
+ expect do
1127
+ expect { described_class.start(['--invalid-option']) }
1128
+ .to output(/Error:.*invalid/).to_stdout
1129
+ end.to raise_error(SystemExit) { |error|
1130
+ expect(error.status).to eq(1)
1131
+ }
1132
+ end
1133
+
1134
+ it 'suggests using --help' do
1135
+ expect do
1136
+ expect { described_class.start(['--invalid-option']) }
1137
+ .to output(/Use --help for usage information/).to_stdout
1138
+ end.to raise_error(SystemExit)
1139
+ end
1140
+ end
1141
+ end
1142
+ end