discourse_image_optim 0.24.4
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.
- checksums.yaml +7 -0
- data/.appveyor.yml +46 -0
- data/.gitignore +18 -0
- data/.rubocop.yml +110 -0
- data/.travis.yml +42 -0
- data/CHANGELOG.markdown +316 -0
- data/CONTRIBUTING.markdown +11 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +358 -0
- data/Vagrantfile +38 -0
- data/bin/image_optim +28 -0
- data/image_optim.gemspec +34 -0
- data/lib/image_optim.rb +267 -0
- data/lib/image_optim/bin_resolver.rb +142 -0
- data/lib/image_optim/bin_resolver/bin.rb +115 -0
- data/lib/image_optim/bin_resolver/comparable_condition.rb +60 -0
- data/lib/image_optim/bin_resolver/error.rb +6 -0
- data/lib/image_optim/bin_resolver/simple_version.rb +31 -0
- data/lib/image_optim/cache.rb +72 -0
- data/lib/image_optim/cache_path.rb +16 -0
- data/lib/image_optim/cmd.rb +122 -0
- data/lib/image_optim/config.rb +219 -0
- data/lib/image_optim/configuration_error.rb +3 -0
- data/lib/image_optim/handler.rb +57 -0
- data/lib/image_optim/hash_helpers.rb +45 -0
- data/lib/image_optim/image_meta.rb +20 -0
- data/lib/image_optim/non_negative_integer_range.rb +11 -0
- data/lib/image_optim/optimized_path.rb +25 -0
- data/lib/image_optim/option_definition.rb +38 -0
- data/lib/image_optim/option_helpers.rb +17 -0
- data/lib/image_optim/path.rb +70 -0
- data/lib/image_optim/runner.rb +139 -0
- data/lib/image_optim/runner/glob_helpers.rb +45 -0
- data/lib/image_optim/runner/option_parser.rb +246 -0
- data/lib/image_optim/space.rb +29 -0
- data/lib/image_optim/true_false_nil.rb +16 -0
- data/lib/image_optim/worker.rb +170 -0
- data/lib/image_optim/worker/advpng.rb +37 -0
- data/lib/image_optim/worker/class_methods.rb +107 -0
- data/lib/image_optim/worker/gifsicle.rb +65 -0
- data/lib/image_optim/worker/jhead.rb +47 -0
- data/lib/image_optim/worker/jpegoptim.rb +63 -0
- data/lib/image_optim/worker/jpegrecompress.rb +49 -0
- data/lib/image_optim/worker/jpegtran.rb +48 -0
- data/lib/image_optim/worker/optipng.rb +53 -0
- data/lib/image_optim/worker/pngcrush.rb +56 -0
- data/lib/image_optim/worker/pngout.rb +40 -0
- data/lib/image_optim/worker/pngquant.rb +61 -0
- data/lib/image_optim/worker/svgo.rb +34 -0
- data/script/template/jquery-2.1.3.min.js +4 -0
- data/script/template/sortable-0.6.0.min.js +2 -0
- data/script/template/worker_analysis.erb +254 -0
- data/script/update_worker_options_in_readme +59 -0
- data/script/worker_analysis +589 -0
- data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +37 -0
- data/spec/image_optim/bin_resolver/simple_version_spec.rb +65 -0
- data/spec/image_optim/bin_resolver_spec.rb +290 -0
- data/spec/image_optim/cache_path_spec.rb +57 -0
- data/spec/image_optim/cache_spec.rb +162 -0
- data/spec/image_optim/cmd_spec.rb +93 -0
- data/spec/image_optim/config_spec.rb +254 -0
- data/spec/image_optim/handler_spec.rb +90 -0
- data/spec/image_optim/hash_helpers_spec.rb +74 -0
- data/spec/image_optim/image_meta_spec.rb +61 -0
- data/spec/image_optim/optimized_path_spec.rb +58 -0
- data/spec/image_optim/option_definition_spec.rb +138 -0
- data/spec/image_optim/option_helpers_spec.rb +25 -0
- data/spec/image_optim/path_spec.rb +103 -0
- data/spec/image_optim/runner/glob_helpers_spec.rb +21 -0
- data/spec/image_optim/runner/option_parser_spec.rb +105 -0
- data/spec/image_optim/space_spec.rb +23 -0
- data/spec/image_optim/worker/optipng_spec.rb +102 -0
- data/spec/image_optim/worker/pngquant_spec.rb +67 -0
- data/spec/image_optim/worker_spec.rb +303 -0
- data/spec/image_optim_spec.rb +259 -0
- data/spec/images/broken_jpeg +1 -0
- data/spec/images/comparison.png +0 -0
- data/spec/images/decompressed.jpeg +0 -0
- data/spec/images/icecream.gif +0 -0
- data/spec/images/image.jpg +0 -0
- data/spec/images/invisiblepixels/generate +24 -0
- data/spec/images/invisiblepixels/image.png +0 -0
- data/spec/images/lena.jpg +0 -0
- data/spec/images/orient/0.jpg +0 -0
- data/spec/images/orient/1.jpg +0 -0
- data/spec/images/orient/2.jpg +0 -0
- data/spec/images/orient/3.jpg +0 -0
- data/spec/images/orient/4.jpg +0 -0
- data/spec/images/orient/5.jpg +0 -0
- data/spec/images/orient/6.jpg +0 -0
- data/spec/images/orient/7.jpg +0 -0
- data/spec/images/orient/8.jpg +0 -0
- data/spec/images/orient/generate +23 -0
- data/spec/images/orient/original.jpg +0 -0
- data/spec/images/quant/64.png +0 -0
- data/spec/images/quant/generate +25 -0
- data/spec/images/rails.png +0 -0
- data/spec/images/test.svg +3 -0
- data/spec/images/transparency1.png +0 -0
- data/spec/images/transparency2.png +0 -0
- data/spec/images/vergroessert.jpg +0 -0
- data/spec/spec_helper.rb +93 -0
- 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
|