discourse_image_optim 0.24.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/.appveyor.yml +46 -0
  3. data/.gitignore +18 -0
  4. data/.rubocop.yml +110 -0
  5. data/.travis.yml +42 -0
  6. data/CHANGELOG.markdown +316 -0
  7. data/CONTRIBUTING.markdown +11 -0
  8. data/Gemfile +16 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.markdown +358 -0
  11. data/Vagrantfile +38 -0
  12. data/bin/image_optim +28 -0
  13. data/image_optim.gemspec +34 -0
  14. data/lib/image_optim.rb +267 -0
  15. data/lib/image_optim/bin_resolver.rb +142 -0
  16. data/lib/image_optim/bin_resolver/bin.rb +115 -0
  17. data/lib/image_optim/bin_resolver/comparable_condition.rb +60 -0
  18. data/lib/image_optim/bin_resolver/error.rb +6 -0
  19. data/lib/image_optim/bin_resolver/simple_version.rb +31 -0
  20. data/lib/image_optim/cache.rb +72 -0
  21. data/lib/image_optim/cache_path.rb +16 -0
  22. data/lib/image_optim/cmd.rb +122 -0
  23. data/lib/image_optim/config.rb +219 -0
  24. data/lib/image_optim/configuration_error.rb +3 -0
  25. data/lib/image_optim/handler.rb +57 -0
  26. data/lib/image_optim/hash_helpers.rb +45 -0
  27. data/lib/image_optim/image_meta.rb +20 -0
  28. data/lib/image_optim/non_negative_integer_range.rb +11 -0
  29. data/lib/image_optim/optimized_path.rb +25 -0
  30. data/lib/image_optim/option_definition.rb +38 -0
  31. data/lib/image_optim/option_helpers.rb +17 -0
  32. data/lib/image_optim/path.rb +70 -0
  33. data/lib/image_optim/runner.rb +139 -0
  34. data/lib/image_optim/runner/glob_helpers.rb +45 -0
  35. data/lib/image_optim/runner/option_parser.rb +246 -0
  36. data/lib/image_optim/space.rb +29 -0
  37. data/lib/image_optim/true_false_nil.rb +16 -0
  38. data/lib/image_optim/worker.rb +170 -0
  39. data/lib/image_optim/worker/advpng.rb +37 -0
  40. data/lib/image_optim/worker/class_methods.rb +107 -0
  41. data/lib/image_optim/worker/gifsicle.rb +65 -0
  42. data/lib/image_optim/worker/jhead.rb +47 -0
  43. data/lib/image_optim/worker/jpegoptim.rb +63 -0
  44. data/lib/image_optim/worker/jpegrecompress.rb +49 -0
  45. data/lib/image_optim/worker/jpegtran.rb +48 -0
  46. data/lib/image_optim/worker/optipng.rb +53 -0
  47. data/lib/image_optim/worker/pngcrush.rb +56 -0
  48. data/lib/image_optim/worker/pngout.rb +40 -0
  49. data/lib/image_optim/worker/pngquant.rb +61 -0
  50. data/lib/image_optim/worker/svgo.rb +34 -0
  51. data/script/template/jquery-2.1.3.min.js +4 -0
  52. data/script/template/sortable-0.6.0.min.js +2 -0
  53. data/script/template/worker_analysis.erb +254 -0
  54. data/script/update_worker_options_in_readme +59 -0
  55. data/script/worker_analysis +589 -0
  56. data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +37 -0
  57. data/spec/image_optim/bin_resolver/simple_version_spec.rb +65 -0
  58. data/spec/image_optim/bin_resolver_spec.rb +290 -0
  59. data/spec/image_optim/cache_path_spec.rb +57 -0
  60. data/spec/image_optim/cache_spec.rb +162 -0
  61. data/spec/image_optim/cmd_spec.rb +93 -0
  62. data/spec/image_optim/config_spec.rb +254 -0
  63. data/spec/image_optim/handler_spec.rb +90 -0
  64. data/spec/image_optim/hash_helpers_spec.rb +74 -0
  65. data/spec/image_optim/image_meta_spec.rb +61 -0
  66. data/spec/image_optim/optimized_path_spec.rb +58 -0
  67. data/spec/image_optim/option_definition_spec.rb +138 -0
  68. data/spec/image_optim/option_helpers_spec.rb +25 -0
  69. data/spec/image_optim/path_spec.rb +103 -0
  70. data/spec/image_optim/runner/glob_helpers_spec.rb +21 -0
  71. data/spec/image_optim/runner/option_parser_spec.rb +105 -0
  72. data/spec/image_optim/space_spec.rb +23 -0
  73. data/spec/image_optim/worker/optipng_spec.rb +102 -0
  74. data/spec/image_optim/worker/pngquant_spec.rb +67 -0
  75. data/spec/image_optim/worker_spec.rb +303 -0
  76. data/spec/image_optim_spec.rb +259 -0
  77. data/spec/images/broken_jpeg +1 -0
  78. data/spec/images/comparison.png +0 -0
  79. data/spec/images/decompressed.jpeg +0 -0
  80. data/spec/images/icecream.gif +0 -0
  81. data/spec/images/image.jpg +0 -0
  82. data/spec/images/invisiblepixels/generate +24 -0
  83. data/spec/images/invisiblepixels/image.png +0 -0
  84. data/spec/images/lena.jpg +0 -0
  85. data/spec/images/orient/0.jpg +0 -0
  86. data/spec/images/orient/1.jpg +0 -0
  87. data/spec/images/orient/2.jpg +0 -0
  88. data/spec/images/orient/3.jpg +0 -0
  89. data/spec/images/orient/4.jpg +0 -0
  90. data/spec/images/orient/5.jpg +0 -0
  91. data/spec/images/orient/6.jpg +0 -0
  92. data/spec/images/orient/7.jpg +0 -0
  93. data/spec/images/orient/8.jpg +0 -0
  94. data/spec/images/orient/generate +23 -0
  95. data/spec/images/orient/original.jpg +0 -0
  96. data/spec/images/quant/64.png +0 -0
  97. data/spec/images/quant/generate +25 -0
  98. data/spec/images/rails.png +0 -0
  99. data/spec/images/test.svg +3 -0
  100. data/spec/images/transparency1.png +0 -0
  101. data/spec/images/transparency2.png +0 -0
  102. data/spec/images/vergroessert.jpg +0 -0
  103. data/spec/spec_helper.rb +93 -0
  104. metadata +281 -0
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+ require 'image_optim/path'
3
+ require 'tempfile'
4
+
5
+ describe ImageOptim::Path do
6
+ include CapabilityCheckHelpers
7
+
8
+ before do
9
+ stub_const('Path', ImageOptim::Path)
10
+ end
11
+
12
+ describe '.convert' do
13
+ it 'returns Path for string' do
14
+ path = 'a'
15
+
16
+ expect(Path.convert(path)).to be_a(Path)
17
+ expect(Path.convert(path)).to eq(Path.new(path))
18
+
19
+ expect(Path.convert(path)).not_to eq(path)
20
+ expect(Path.convert(path)).not_to be(path)
21
+ end
22
+
23
+ it 'returns Path for Pathname' do
24
+ pathname = Pathname.new('a')
25
+
26
+ expect(Path.convert(pathname)).to be_a(Path)
27
+ expect(Path.convert(pathname)).to eq(Path.new(pathname))
28
+
29
+ expect(Path.convert(pathname)).to eq(pathname)
30
+ expect(Path.convert(pathname)).not_to be(pathname)
31
+ end
32
+
33
+ it 'returns same instance for Path' do
34
+ image_path = Path.new('a')
35
+
36
+ expect(Path.convert(image_path)).to be_a(Path)
37
+ expect(Path.convert(image_path)).to eq(Path.new(image_path))
38
+
39
+ expect(Path.convert(image_path)).to eq(image_path)
40
+ expect(Path.convert(image_path)).to be(image_path)
41
+ end
42
+ end
43
+
44
+ describe '#binread' do
45
+ it 'reads binary data' do
46
+ data = (0..255).to_a.pack('c*')
47
+
48
+ path = Path.temp_file_path
49
+ path.binwrite(data)
50
+
51
+ expect(path.binread).to eq(data)
52
+ if ''.respond_to?(:encoding)
53
+ expect(path.binread.encoding).to eq(Encoding.find('ASCII-8BIT'))
54
+ end
55
+ end
56
+ end
57
+
58
+ describe '#replace' do
59
+ let(:src){ Path.temp_file_path }
60
+ let(:dst){ Path.temp_file_path }
61
+
62
+ it 'moves data to destination' do
63
+ src.write('src')
64
+
65
+ src.replace(dst)
66
+
67
+ expect(dst.read).to eq('src')
68
+ end
69
+
70
+ it 'removes original file' do
71
+ src.replace(dst)
72
+
73
+ expect(src).to_not exist
74
+ end
75
+
76
+ it 'preserves attributes of destination file' do
77
+ skip 'full file modes are not support' unless any_file_modes_allowed?
78
+ mode = 0o666
79
+
80
+ dst.chmod(mode)
81
+
82
+ src.replace(dst)
83
+
84
+ got = dst.stat.mode & 0o777
85
+ expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
86
+ end
87
+
88
+ it 'does not preserve mtime of destination file' do
89
+ time = src.mtime
90
+
91
+ dst.utime(time - 1000, time - 1000)
92
+
93
+ src.replace(dst)
94
+
95
+ expect(dst.mtime).to be >= time
96
+ end
97
+
98
+ it 'changes inode of destination' do
99
+ skip 'inodes are not supported' unless inodes_supported?
100
+ expect{ src.replace(dst) }.to change{ dst.stat.ino }
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+ require 'image_optim/runner/glob_helpers'
3
+
4
+ describe ImageOptim::Runner::GlobHelpers do
5
+ describe '.expand_braces' do
6
+ {
7
+ 'hello.world' => %w[hello.world],
8
+ '{hello,.world}' => %w[hello .world],
9
+ 'hello{.,!}world' => %w[hello.world hello!world],
10
+ '{a,b},{c,d}' => %w[a,c b,c a,d b,d],
11
+ '{{a,b},{c,d}}' => %w[a b c d],
12
+ '{a,b,{c,d}}' => %w[a b c d],
13
+ '{\{a,b\},\{c,d\}}' => %w[\\{a b\\} \\{c d\\}],
14
+ 'test{ing,}' => %w[testing test],
15
+ }.each do |glob, expected|
16
+ it "expands #{glob}" do
17
+ expect(described_class.expand_braces(glob)).to match_array(expected)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+ require 'image_optim/runner/option_parser'
3
+
4
+ describe ImageOptim::Runner::OptionParser do
5
+ before do
6
+ stub_const('OptionParser', ImageOptim::Runner::OptionParser)
7
+ end
8
+
9
+ def exit_with_status(status)
10
+ raise_error(SystemExit) do |e|
11
+ expect(e.status).to eq(status)
12
+ end
13
+ end
14
+
15
+ describe '.parse!' do
16
+ it 'returns empty hash for arguments without options' do
17
+ args = %w[foo bar]
18
+ expect(OptionParser.parse!(args)).to eq({})
19
+ expect(args).to eq(%w[foo bar])
20
+ end
21
+
22
+ it 'removes options from arguments' do
23
+ args = %w[-r foo bar]
24
+ OptionParser.parse!(args)
25
+ expect(args).to eq(%w[foo bar])
26
+ end
27
+
28
+ it 'stops parsing options after --' do
29
+ args = %w[-- -r foo bar]
30
+ OptionParser.parse!(args)
31
+ expect(args).to eq(%w[-r foo bar])
32
+ end
33
+
34
+ describe 'boolean option recursive' do
35
+ %w[-r -R --recursive].each do |flag|
36
+ it "is parsed from #{flag}" do
37
+ args = %W[#{flag} foo bar]
38
+ expect(OptionParser.parse!(args)).to eq(:recursive => true)
39
+ expect(args).to eq(%w[foo bar])
40
+ end
41
+ end
42
+ end
43
+
44
+ describe 'numeric option threads' do
45
+ it 'is parsed with space separator' do
46
+ args = %w[--threads 616 foo bar]
47
+ expect(OptionParser.parse!(args)).to eq(:threads => 616)
48
+ expect(args).to eq(%w[foo bar])
49
+ end
50
+
51
+ it 'is parsed with equal separator' do
52
+ args = %w[--threads=616 foo bar]
53
+ expect(OptionParser.parse!(args)).to eq(:threads => 616)
54
+ expect(args).to eq(%w[foo bar])
55
+ end
56
+
57
+ it 'is parsed with no- prefix' do
58
+ args = %w[--no-threads 616 foo bar]
59
+ expect(OptionParser.parse!(args)).to eq(:threads => false)
60
+ expect(args).to eq(%w[616 foo bar])
61
+ end
62
+ end
63
+
64
+ describe 'help option' do
65
+ it 'prints help text to stdout and exits' do
66
+ parser = OptionParser.new({})
67
+ expect(OptionParser).to receive(:new).and_return(parser)
68
+
69
+ help = double
70
+ expect(parser).to receive(:help).and_return(help)
71
+
72
+ expect do
73
+ OptionParser.parse!(%w[--help])
74
+ end.to exit_with_status(0) &
75
+ output("#{help}\n").to_stdout
76
+ end
77
+ end
78
+
79
+ describe 'wrong option' do
80
+ it 'prints help text to stdout and exits' do
81
+ parser = OptionParser.new({})
82
+ expect(OptionParser).to receive(:new).and_return(parser)
83
+
84
+ help = double
85
+ expect(parser).to receive(:help).and_return(help)
86
+
87
+ expect do
88
+ OptionParser.parse!(%w[--unknown-option])
89
+ end.to exit_with_status(1) &
90
+ output("invalid option: --unknown-option\n\n#{help}\n").to_stderr
91
+ end
92
+ end
93
+ end
94
+
95
+ describe '#help' do
96
+ it 'returns wrapped text' do
97
+ parser = OptionParser.new({})
98
+
99
+ allow(parser).to receive(:terminal_columns).and_return(80)
100
+
101
+ expect(parser.help.split("\n")).
102
+ to all(satisfy{ |line| line.length <= 80 })
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+ require 'image_optim/space'
3
+
4
+ describe ImageOptim::Space do
5
+ describe '.space' do
6
+ {
7
+ 0 => ' ',
8
+ 1 => ' 1B',
9
+ 10 => ' 10B',
10
+ 100 => ' 100B',
11
+ 1_000 => ' 1000B',
12
+ 10_000 => ' 9.8K',
13
+ 100_000 => ' 97.7K',
14
+ 1_000_000 => '976.6K',
15
+ 10_000_000 => ' 9.5M',
16
+ 100_000_000 => ' 95.4M',
17
+ }.each do |size, space|
18
+ it "converts #{size} to #{space}" do
19
+ expect(described_class.space(size)).to eq(space)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+ require 'image_optim/worker/optipng'
3
+ require 'image_optim/path'
4
+
5
+ describe ImageOptim::Worker::Optipng do
6
+ describe 'strip option' do
7
+ subject{ described_class.new(ImageOptim.new, options) }
8
+
9
+ let(:options){ {} }
10
+ let(:optipng_version){ '0.7' }
11
+ let(:src){ instance_double(ImageOptim::Path, :copy => nil) }
12
+ let(:dst){ instance_double(ImageOptim::Path) }
13
+
14
+ before do
15
+ optipng_bin = instance_double(ImageOptim::BinResolver::Bin)
16
+ allow(subject).to receive(:resolve_bin!).
17
+ with(:optipng).and_return(optipng_bin)
18
+ allow(optipng_bin).to receive(:version).
19
+ and_return(ImageOptim::BinResolver::SimpleVersion.new(optipng_version))
20
+
21
+ allow(subject).to receive(:optimized?)
22
+ end
23
+
24
+ context 'by default' do
25
+ it 'should add -strip all to arguments' do
26
+ expect(subject).to receive(:execute) do |_bin, *args|
27
+ expect(args.join(' ')).to match(/(^| )-strip all($| )/)
28
+ end
29
+
30
+ subject.optimize(src, dst)
31
+ end
32
+ end
33
+
34
+ context 'when strip is disabled' do
35
+ let(:options){ {:strip => false} }
36
+
37
+ it 'should not add -strip all to arguments' do
38
+ expect(subject).to receive(:execute) do |_bin, *args|
39
+ expect(args.join(' ')).not_to match(/(^| )-strip all($| )/)
40
+ end
41
+
42
+ subject.optimize(src, dst)
43
+ end
44
+ end
45
+
46
+ context 'when optipng version is < 0.7' do
47
+ let(:optipng_version){ '0.6.999' }
48
+
49
+ it 'should not add -strip all to arguments' do
50
+ expect(subject).to receive(:execute) do |_bin, *args|
51
+ expect(args.join(' ')).not_to match(/(^| )-strip all($| )/)
52
+ end
53
+
54
+ subject.optimize(src, dst)
55
+ end
56
+ end
57
+ end
58
+
59
+ describe '#optimized?' do
60
+ let(:src){ instance_double(ImageOptim::Path, src_options) }
61
+ let(:dst){ instance_double(ImageOptim::Path, dst_options) }
62
+ let(:src_options){ {:size => 10} }
63
+ let(:dst_options){ {:size? => 9} }
64
+ let(:instance){ described_class.new(ImageOptim.new, instance_options) }
65
+ let(:instance_options){ {} }
66
+
67
+ subject{ instance.optimized?(src, dst) }
68
+
69
+ context 'when interlace option is enabled' do
70
+ let(:instance_options){ {:interlace => true} }
71
+
72
+ context 'when dst is empty' do
73
+ let(:dst_options){ {:size? => nil} }
74
+ it{ is_expected.to be_falsy }
75
+ end
76
+
77
+ context 'when dst is not empty' do
78
+ let(:dst_options){ {:size? => 20} }
79
+ it{ is_expected.to be_truthy }
80
+ end
81
+ end
82
+
83
+ context 'when interlace option is disabled' do
84
+ let(:instance_options){ {:interlace => false} }
85
+
86
+ context 'when dst is empty' do
87
+ let(:dst_options){ {:size? => nil} }
88
+ it{ is_expected.to be_falsy }
89
+ end
90
+
91
+ context 'when dst is greater than or equal to src' do
92
+ let(:dst_options){ {:size? => 10} }
93
+ it{ is_expected.to be_falsy }
94
+ end
95
+
96
+ context 'when dst is less than src' do
97
+ let(:dst_options){ {:size? => 9} }
98
+ it{ is_expected.to be_truthy }
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+ require 'image_optim/worker/pngquant'
3
+
4
+ describe ImageOptim::Worker::Pngquant do
5
+ describe 'quality option' do
6
+ describe 'default' do
7
+ subject{ described_class::QUALITY_OPTION.default }
8
+
9
+ it{ is_expected.to match(/100\.\.100.*0\.\.100/) }
10
+ end
11
+
12
+ describe 'value' do
13
+ let(:subject){ described_class.new(ImageOptim.new, options).quality }
14
+
15
+ context 'when lossy not allowed' do
16
+ context 'by default' do
17
+ let(:options){ {} }
18
+
19
+ it{ is_expected.to eq(100..100) }
20
+ end
21
+
22
+ context 'when value is passed through options' do
23
+ let(:options){ {:quality => 10..90} }
24
+
25
+ it 'warns and keeps default' do
26
+ expect_any_instance_of(described_class).
27
+ to receive(:warn).with(/ignored in lossless mode/)
28
+ is_expected.to eq(100..100)
29
+ end
30
+ end
31
+ end
32
+
33
+ context 'when lossy allowed' do
34
+ context 'by default' do
35
+ let(:options){ {:allow_lossy => true} }
36
+
37
+ it{ is_expected.to eq(0..100) }
38
+ end
39
+
40
+ context 'when value is passed through options' do
41
+ let(:options){ {:allow_lossy => true, :quality => 10..90} }
42
+
43
+ it 'sets the value without warning' do
44
+ expect_any_instance_of(described_class).not_to receive(:warn)
45
+ is_expected.to eq(10..90)
46
+ end
47
+ end
48
+
49
+ context 'when passed range begin is less than 0' do
50
+ let(:options){ {:allow_lossy => true, :quality => -50..50} }
51
+
52
+ it 'sets begin to 0' do
53
+ is_expected.to eq(0..50)
54
+ end
55
+ end
56
+
57
+ context 'when passed range end is more than 100' do
58
+ let(:options){ {:allow_lossy => true, :quality => 50..150} }
59
+
60
+ it 'sets end to 100' do
61
+ is_expected.to eq(50..100)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,303 @@
1
+ require 'spec_helper'
2
+ require 'image_optim/worker'
3
+ require 'image_optim/bin_resolver'
4
+
5
+ describe ImageOptim::Worker do
6
+ before do
7
+ stub_const('Worker', ImageOptim::Worker)
8
+ stub_const('BinResolver', ImageOptim::BinResolver)
9
+
10
+ # don't add to list of wokers
11
+ allow(ImageOptim::Worker).to receive(:inherited)
12
+ end
13
+
14
+ describe '#initialize' do
15
+ it 'expects first argument to be an instanace of ImageOptim' do
16
+ expect do
17
+ Worker.new(double)
18
+ end.to raise_error ArgumentError
19
+ end
20
+ end
21
+
22
+ describe '#options' do
23
+ it 'returns a Hash with options' do
24
+ worker_class = Class.new(Worker) do
25
+ option(:one, 1, 'One')
26
+ option(:two, 2, 'Two')
27
+ option(:three, 3, 'Three')
28
+ end
29
+
30
+ worker = worker_class.new(ImageOptim.new, :three => '...')
31
+
32
+ expect(worker.options).to eq(:one => 1, :two => 2, :three => '...')
33
+ end
34
+ end
35
+
36
+ describe '#optimize' do
37
+ it 'raises NotImplementedError' do
38
+ worker = Worker.new(ImageOptim.new, {})
39
+
40
+ expect do
41
+ worker.optimize(double, double)
42
+ end.to raise_error NotImplementedError
43
+ end
44
+ end
45
+
46
+ describe '#image_formats' do
47
+ {
48
+ 'GifOptim' => :gif,
49
+ 'JpegOptim' => :jpeg,
50
+ 'PngOptim' => :png,
51
+ 'SvgOptim' => :svg,
52
+ }.each do |class_name, image_format|
53
+ it "detects if class name contains #{image_format}" do
54
+ worker = stub_const(class_name, Class.new(Worker)).new(ImageOptim.new)
55
+ expect(worker.image_formats).to eq([image_format])
56
+ end
57
+ end
58
+
59
+ it 'fails if class name does not contain known type' do
60
+ worker = stub_const('TiffOptim', Class.new(Worker)).new(ImageOptim.new)
61
+ expect{ worker.image_formats }.to raise_error(/can't guess/)
62
+ end
63
+ end
64
+
65
+ describe '#inspect' do
66
+ it 'returns inspect String containing options' do
67
+ stub_const('DefOptim', Class.new(Worker) do
68
+ option(:one, 1, 'One')
69
+ option(:two, 2, 'Two')
70
+ option(:three, 3, 'Three')
71
+ end)
72
+
73
+ worker = DefOptim.new(ImageOptim.new, :three => '...')
74
+
75
+ expect(worker.inspect).to eq('#<DefOptim @one=1, @two=2, @three="...">')
76
+ end
77
+ end
78
+
79
+ describe '.inherited' do
80
+ it 'adds subclasses to klasses' do
81
+ base_class = Class.new{ extend ImageOptim::Worker::ClassMethods }
82
+ expect(base_class.klasses.to_a).to eq([])
83
+
84
+ worker_class = Class.new(base_class)
85
+ expect(base_class.klasses.to_a).to eq([worker_class])
86
+ end
87
+ end
88
+
89
+ describe '.create_all_by_format' do
90
+ it 'passes arguments to create_all' do
91
+ image_optim = double
92
+ options_proc = proc{ true }
93
+
94
+ expect(Worker).to receive(:create_all) do |arg, &block|
95
+ expect(arg).to eq(image_optim)
96
+ expect(block).to eq(options_proc)
97
+ []
98
+ end
99
+
100
+ Worker.create_all_by_format(image_optim, &options_proc)
101
+ end
102
+
103
+ it 'create hash by format' do
104
+ workers = [
105
+ double(:image_formats => [:a]),
106
+ double(:image_formats => [:a, :b]),
107
+ double(:image_formats => [:b, :c]),
108
+ ]
109
+
110
+ expect(Worker).to receive(:create_all).and_return(workers)
111
+
112
+ worker_by_format = {
113
+ :a => [workers[0], workers[1]],
114
+ :b => [workers[1], workers[2]],
115
+ :c => [workers[2]],
116
+ }
117
+
118
+ expect(Worker.create_all_by_format(double)).to eq(worker_by_format)
119
+ end
120
+ end
121
+
122
+ describe '.create_all' do
123
+ def worker_double(override = {})
124
+ stubs = {:resolve_used_bins! => nil, :run_order => 0}.merge(override)
125
+ instance_double(Worker, stubs)
126
+ end
127
+
128
+ def worker_class_doubles(workers)
129
+ workers.map{ |worker| class_double(Worker, :init => worker) }
130
+ end
131
+
132
+ let(:image_optim){ double(:allow_lossy => false, :timeout => 30) }
133
+
134
+ it 'creates all workers for which options_proc returns true' do
135
+ workers = Array.new(3){ worker_double }
136
+ klasses = worker_class_doubles(workers)
137
+ options_proc = proc do |klass|
138
+ klass == klasses[1] ? {:disable => true} : {}
139
+ end
140
+
141
+ allow(Worker).to receive(:klasses).and_return(klasses)
142
+
143
+ expect(Worker.create_all(image_optim, &options_proc)).
144
+ to eq([workers[0], workers[2]])
145
+ end
146
+
147
+ it 'handles workers initializing multiple instances' do
148
+ workers = [
149
+ worker_double,
150
+ [worker_double, worker_double, worker_double],
151
+ worker_double,
152
+ ]
153
+ klasses = worker_class_doubles(workers)
154
+
155
+ allow(Worker).to receive(:klasses).and_return(klasses)
156
+
157
+ expect(Worker.create_all(image_optim){ {} }).
158
+ to eq(workers.flatten)
159
+ end
160
+
161
+ describe 'with missing workers' do
162
+ let(:workers) do
163
+ %w[a b c c].map do |bin|
164
+ worker = worker_double
165
+ unless bin == 'b'
166
+ allow(worker).to receive(:resolve_used_bins!).
167
+ and_raise(BinResolver::BinNotFound, "not found #{bin}")
168
+ end
169
+ worker
170
+ end
171
+ end
172
+ let(:klasses){ worker_class_doubles(workers) }
173
+
174
+ before do
175
+ allow(Worker).to receive(:klasses).and_return(klasses)
176
+ end
177
+
178
+ describe 'if skip_missing_workers is true' do
179
+ it 'shows deduplicated warnings and returns resolved workers ' do
180
+ allow(image_optim).to receive(:skip_missing_workers).and_return(true)
181
+
182
+ expect(Worker).to receive(:warn).once.with('not found a')
183
+ expect(Worker).to receive(:warn).once.with('not found c')
184
+
185
+ expect(Worker.create_all(image_optim){ {} }).to eq([workers[1]])
186
+ end
187
+ end
188
+
189
+ describe 'if skip_missing_workers is false' do
190
+ it 'fails with a joint exception of deduplicated messages' do
191
+ allow(image_optim).to receive(:skip_missing_workers).and_return(false)
192
+
193
+ expect do
194
+ Worker.create_all(image_optim){ {} }
195
+ end.to raise_error(BinResolver::Error, [
196
+ 'Bin resolving errors:',
197
+ 'not found a',
198
+ 'not found c',
199
+ ].join("\n"))
200
+ end
201
+ end
202
+ end
203
+
204
+ it 'orders workers by run_order' do
205
+ run_orders = [10, -10, 0, 0, 0, 10, -10]
206
+ workers = run_orders.map do |run_order|
207
+ worker_double(:run_order => run_order)
208
+ end
209
+ klasses_list = worker_class_doubles(workers)
210
+
211
+ [
212
+ klasses_list,
213
+ klasses_list.reverse,
214
+ klasses_list.shuffle,
215
+ ].each do |klasses|
216
+ allow(Worker).to receive(:klasses).and_return(klasses)
217
+
218
+ expected_order = klasses.map(&:init).sort_by.with_index do |worker, i|
219
+ [worker.run_order, i]
220
+ end
221
+
222
+ expect(Worker.create_all(image_optim){ {} }).to eq(expected_order)
223
+ end
224
+ end
225
+
226
+ describe 'configuring workers' do
227
+ [:allow_lossy, :timeout].each do |option|
228
+ it "passes #{option} if worker has such attribute" do
229
+ klasses = worker_class_doubles([worker_double, worker_double])
230
+
231
+ allow(Worker).to receive(:klasses).and_return(klasses)
232
+
233
+ klasses[0].send(:attr_reader, option)
234
+ expect(klasses[0]).to receive(:init).
235
+ with(image_optim, hash_including(option))
236
+ expect(klasses[1]).to receive(:init).
237
+ with(image_optim, hash_not_including(option))
238
+
239
+ Worker.create_all(image_optim){ {} }
240
+ end
241
+ end
242
+
243
+ it 'allows overriding allow_lossy per worker' do
244
+ klasses = worker_class_doubles([worker_double, worker_double])
245
+ options_proc = proc do |klass|
246
+ klass == klasses[1] ? {:allow_lossy => :b} : {}
247
+ end
248
+
249
+ allow(Worker).to receive(:klasses).and_return(klasses)
250
+
251
+ klasses.each{ |klass| klass.send(:attr_reader, :allow_lossy) }
252
+ expect(klasses[0]).to receive(:init).
253
+ with(image_optim, hash_including(:allow_lossy => false))
254
+ expect(klasses[1]).to receive(:init).
255
+ with(image_optim, hash_including(:allow_lossy => :b))
256
+
257
+ Worker.create_all(image_optim, &options_proc)
258
+ end
259
+
260
+ it 'allows overriding timeout per worker' do
261
+ klasses = worker_class_doubles([worker_double, worker_double])
262
+ options_proc = proc do |klass|
263
+ klass == klasses[1] ? {:timeout => 50} : {}
264
+ end
265
+
266
+ allow(Worker).to receive(:klasses).and_return(klasses)
267
+
268
+ klasses.each{ |klass| klass.send(:attr_reader, :timeout) }
269
+ expect(klasses[0]).to receive(:init).
270
+ with(image_optim, hash_including(:timeout => 30))
271
+ expect(klasses[1]).to receive(:init).
272
+ with(image_optim, hash_including(:timeout => 50))
273
+
274
+ Worker.create_all(image_optim, &options_proc)
275
+ end
276
+ end
277
+ end
278
+
279
+ describe '.option' do
280
+ it 'runs option block in context of worker' do
281
+ stub_const('Abc', Class.new(Worker) do
282
+ option(:test, 1, 'Test context') do |_v|
283
+ some_instance_method
284
+ end
285
+ end)
286
+
287
+ expect_any_instance_of(Abc).
288
+ to receive(:some_instance_method).and_return(20)
289
+ expect(Abc.new(ImageOptim.new).test).to eq(20)
290
+ end
291
+
292
+ it 'returns instance of OptionDefinition' do
293
+ definition = nil
294
+ Class.new(Worker) do
295
+ definition = option(:test, 1, 'Test'){ |v| v }
296
+ end
297
+
298
+ expect(definition).to be_an(ImageOptim::OptionDefinition)
299
+ expect(definition.name).to eq(:test)
300
+ expect(definition.default).to eq(1)
301
+ end
302
+ end
303
+ end