image_optim 0.22.1 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +8 -8
  2. data/.appveyor.yml +95 -0
  3. data/.rubocop.yml +3 -0
  4. data/.travis.yml +27 -22
  5. data/CHANGELOG.markdown +10 -0
  6. data/CONTRIBUTING.markdown +2 -1
  7. data/Gemfile +1 -1
  8. data/README.markdown +10 -2
  9. data/image_optim.gemspec +4 -4
  10. data/lib/image_optim.rb +32 -16
  11. data/lib/image_optim/bin_resolver/bin.rb +11 -4
  12. data/lib/image_optim/cache.rb +71 -0
  13. data/lib/image_optim/cache_path.rb +16 -0
  14. data/lib/image_optim/config.rb +12 -2
  15. data/lib/image_optim/handler.rb +1 -1
  16. data/lib/image_optim/image_meta.rb +5 -10
  17. data/lib/image_optim/optimized_path.rb +25 -0
  18. data/lib/image_optim/path.rb +70 -0
  19. data/lib/image_optim/runner/option_parser.rb +13 -0
  20. data/lib/image_optim/worker.rb +5 -8
  21. data/lib/image_optim/worker/class_methods.rb +3 -1
  22. data/lib/image_optim/worker/jpegoptim.rb +3 -0
  23. data/lib/image_optim/worker/jpegrecompress.rb +3 -0
  24. data/lib/image_optim/worker/pngquant.rb +3 -0
  25. data/script/worker_analysis +10 -9
  26. data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +1 -1
  27. data/spec/image_optim/bin_resolver/simple_version_spec.rb +48 -40
  28. data/spec/image_optim/bin_resolver_spec.rb +190 -172
  29. data/spec/image_optim/cache_path_spec.rb +59 -0
  30. data/spec/image_optim/cache_spec.rb +159 -0
  31. data/spec/image_optim/cmd_spec.rb +11 -7
  32. data/spec/image_optim/config_spec.rb +92 -71
  33. data/spec/image_optim/handler_spec.rb +3 -6
  34. data/spec/image_optim/image_meta_spec.rb +61 -0
  35. data/spec/image_optim/optimized_path_spec.rb +58 -0
  36. data/spec/image_optim/option_helpers_spec.rb +25 -0
  37. data/spec/image_optim/path_spec.rb +105 -0
  38. data/spec/image_optim/railtie_spec.rb +6 -6
  39. data/spec/image_optim/runner/glob_helpers_spec.rb +2 -6
  40. data/spec/image_optim/runner/option_parser_spec.rb +3 -3
  41. data/spec/image_optim/space_spec.rb +16 -18
  42. data/spec/image_optim/worker/optipng_spec.rb +3 -3
  43. data/spec/image_optim/worker/pngquant_spec.rb +47 -7
  44. data/spec/image_optim/worker_spec.rb +114 -17
  45. data/spec/image_optim_spec.rb +58 -69
  46. data/spec/images/broken_jpeg +1 -0
  47. data/spec/spec_helper.rb +40 -10
  48. metadata +30 -8
  49. data/lib/image_optim/image_path.rb +0 -68
  50. data/spec/image_optim/image_path_spec.rb +0 -54
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+ require 'image_optim/cache_path'
3
+ require 'tempfile'
4
+
5
+ describe ImageOptim::CachePath do
6
+ include CapabilityCheckHelpers
7
+
8
+ before do
9
+ stub_const('CachePath', ImageOptim::CachePath)
10
+ end
11
+
12
+ describe '#replace' do
13
+ let(:src){ CachePath.temp_file_path }
14
+ let(:dst){ CachePath.temp_file_path }
15
+
16
+ it 'moves data to destination' do
17
+ src.write('src')
18
+
19
+ src.replace(dst)
20
+
21
+ expect(dst.read).to eq('src')
22
+ end
23
+
24
+ it 'does not remove original file' do
25
+ src.replace(dst)
26
+
27
+ expect(src).to exist
28
+ end
29
+
30
+ it 'preserves attributes of destination file' do
31
+ skip 'full file modes are not support' unless any_file_modes_allowed?
32
+ mode = 0o666
33
+
34
+ dst.chmod(mode)
35
+
36
+ src.replace(dst)
37
+
38
+ got = dst.stat.mode & 0o777
39
+ expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
40
+ end
41
+
42
+ it 'does not preserve mtime of destination file' do
43
+ time = src.mtime
44
+
45
+ dst.utime(time - 1000, time - 1000)
46
+
47
+ src.replace(dst)
48
+
49
+ expect(dst.mtime).to be >= time
50
+ end
51
+
52
+ it 'changes inode of destination' do
53
+ skip 'inodes are not supported' unless inodes_supported?
54
+ expect do
55
+ src.replace(dst)
56
+ end.to change{ dst.stat.ino }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,159 @@
1
+ require 'spec_helper'
2
+ require 'fspath'
3
+ require 'image_optim/cache'
4
+ require 'image_optim/path'
5
+
6
+ describe ImageOptim::Cache do
7
+ before do
8
+ stub_const('Cache', ImageOptim::Cache)
9
+ stub_const('CachePath', ImageOptim::CachePath)
10
+ end
11
+
12
+ let(:tmp_file){ double('/somewhere/tmp/foo/bar', :rename => 0) }
13
+
14
+ let(:cache_dir) do
15
+ dir = '/somewhere/cache'
16
+ allow(FileUtils).to receive(:mkpath).with(Regexp.new(Regexp.escape(dir)))
17
+ allow(FileUtils).to receive(:touch)
18
+ allow(FSPath).to receive(:temp_file_path) do
19
+ tmp_file
20
+ end
21
+ FSPath.new(dir)
22
+ end
23
+
24
+ let(:original) do
25
+ original = double('/somewhere/original', :image_format => :ext)
26
+ allow(Digest::SHA1).to receive(:file).with(original) do
27
+ Digest::SHA1.new << 'some content!'
28
+ end
29
+ original
30
+ end
31
+
32
+ let(:optimized) do
33
+ double('/somewhere/optimized', :format => :ext, :basename => 'optimized')
34
+ end
35
+
36
+ let(:cached) do
37
+ cached = cache_dir / digest
38
+ allow(Digest::SHA1).to receive(:file).with(cached) do
39
+ Digest::SHA1.new << 'some optimized content!'
40
+ end
41
+ CachePath.convert(cached)
42
+ end
43
+
44
+ context 'when cache is disabled (default)' do
45
+ let(:image_optim) do
46
+ double(:image_optim, :cache_dir => nil, :cache_worker_digests => false)
47
+ end
48
+ let(:cache){ Cache.new(image_optim, double) }
49
+
50
+ describe :fetch do
51
+ it 'always return block' do
52
+ expect(cache.fetch(original){ optimized }).to be optimized
53
+ end
54
+ end
55
+
56
+ describe :fetch do
57
+ it 'does not write to disk' do
58
+ expect(FileUtils).not_to receive(:mv)
59
+ expect(FileUtils).not_to receive(:touch)
60
+ expect(cache.fetch(original){ optimized })
61
+ end
62
+ end
63
+ end
64
+
65
+ shared_examples 'an enabled cache' do
66
+ context 'when cached file does not exist' do
67
+ describe :fetch do
68
+ it 'writes to cache when file is optimizable' do
69
+ cached_s = cached.to_s
70
+ expect(FileTest).to receive(:file?).with(cached_s).and_return(false)
71
+ expect(FileTest).not_to receive(:size?).with(cached_s)
72
+ expect(FileUtils).to receive(:mv).with(anything, tmp_file)
73
+ expect(tmp_file).to receive(:rename).with(cached)
74
+
75
+ expect(cache.fetch(original){ optimized }).to eq(cached)
76
+ end
77
+
78
+ it 'writes an empty file to cache when file is already optimized' do
79
+ cached_s = cached.to_s
80
+ expect(FileTest).to receive(:file?).with(cached_s).and_return(false)
81
+ expect(FileTest).not_to receive(:size?).with(cached_s)
82
+ expect(FileUtils).to receive(:mv).with(anything, tmp_file)
83
+ expect(tmp_file).to receive(:rename).with(cached)
84
+
85
+ expect(cache.fetch(original){ optimized }).to eq(cached)
86
+ end
87
+ end
88
+ end
89
+
90
+ context 'when cached file exists (options and/or workers match)' do
91
+ describe(:fetch) do
92
+ it 'returns cached file' do
93
+ cached_s = cached.to_s
94
+ allow(FileTest).to receive(:file?).with(cached_s).and_return(true)
95
+ allow(FileTest).to receive(:size?).with(cached_s).and_return(1234)
96
+ expect(FileUtils).not_to receive(:mv)
97
+ expect(File).not_to receive(:rename)
98
+
99
+ expect(cache.fetch(original){}).to eq(cached)
100
+ end
101
+
102
+ it 'returns nil when file is already optimized' do
103
+ cached_s = cached.to_s
104
+ allow(FileTest).to receive(:file?).with(cached_s).and_return(true)
105
+ allow(FileTest).to receive(:size?).with(cached_s).and_return(nil)
106
+ expect(FileUtils).not_to receive(:mv)
107
+ expect(File).not_to receive(:rename)
108
+
109
+ expect(cache.fetch(original){}).to eq(nil)
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ context 'when cache is enabled (without worker digests)' do
116
+ let(:image_optim) do
117
+ double(:image_optim,
118
+ :cache_dir => cache_dir, :cache_worker_digests => false)
119
+ end
120
+ let(:cache) do
121
+ cache = Cache.new(image_optim, {})
122
+ allow(cache).
123
+ to receive(:options_by_format).
124
+ with(original.image_format).
125
+ and_return('some options!')
126
+ allow(cache).
127
+ to receive(:bins_by_format).
128
+ with(original.image_format).
129
+ and_return('some bins!')
130
+ cache
131
+ end
132
+ let(:digest){ cache.send(:digest, original, original.image_format) }
133
+
134
+ it_behaves_like 'an enabled cache'
135
+ end
136
+
137
+ context 'when cache is enabled (with worker digests)' do
138
+ let(:image_optim) do
139
+ double(:image_optim,
140
+ :cache_dir => cache_dir,
141
+ :cache_worker_digests => true)
142
+ end
143
+ let(:cache) do
144
+ cache = Cache.new(image_optim, {})
145
+ allow(cache).
146
+ to receive(:options_by_format).
147
+ with(original.image_format).
148
+ and_return('some options!')
149
+ allow(cache).
150
+ to receive(:bins_by_format).
151
+ with(original.image_format).
152
+ and_return('some bins!')
153
+ cache
154
+ end
155
+ let(:digest){ cache.send(:digest, original, original.image_format) }
156
+
157
+ it_behaves_like 'an enabled cache'
158
+ end
159
+ end
@@ -2,6 +2,8 @@ require 'spec_helper'
2
2
  require 'image_optim/cmd'
3
3
 
4
4
  describe ImageOptim::Cmd do
5
+ include CapabilityCheckHelpers
6
+
5
7
  before do
6
8
  stub_const('Cmd', ImageOptim::Cmd)
7
9
  end
@@ -12,7 +14,7 @@ describe ImageOptim::Cmd do
12
14
  end
13
15
  end
14
16
 
15
- describe :run do
17
+ describe '.run' do
16
18
  it 'calls system and returns result' do
17
19
  status = double
18
20
  expect(Cmd).to receive(:system).with('cmd', 'arg').and_return(status)
@@ -21,24 +23,25 @@ describe ImageOptim::Cmd do
21
23
  end
22
24
 
23
25
  it 'returns process success status' do
24
- expect(Cmd.run('sh -c exit\ 0')).to eq(true)
26
+ expect(Cmd.run('sh -c "exit 0"')).to eq(true)
25
27
  expect($CHILD_STATUS.exitstatus).to eq(0)
26
28
 
27
- expect(Cmd.run('sh -c exit\ 1')).to eq(false)
29
+ expect(Cmd.run('sh -c "exit 1"')).to eq(false)
28
30
  expect($CHILD_STATUS.exitstatus).to eq(1)
29
31
 
30
- expect(Cmd.run('sh -c exit\ 66')).to eq(false)
32
+ expect(Cmd.run('sh -c "exit 66"')).to eq(false)
31
33
  expect($CHILD_STATUS.exitstatus).to eq(66)
32
34
  end
33
35
 
34
36
  it 'raises SignalException if process terminates after signal' do
37
+ skip 'signals are not supported' unless signals_supported?
35
38
  expect_int_exception do
36
39
  Cmd.run('kill -s INT $$')
37
40
  end
38
41
  end
39
42
  end
40
43
 
41
- describe :capture do
44
+ describe '.capture' do
42
45
  it 'calls ` and returns result' do
43
46
  output = double
44
47
  expect(Cmd).to receive(:`).with('cmd arg arg+').and_return(output)
@@ -50,14 +53,15 @@ describe ImageOptim::Cmd do
50
53
  expect(Cmd.capture('echo test')).to eq("test\n")
51
54
  expect($CHILD_STATUS.exitstatus).to eq(0)
52
55
 
53
- expect(Cmd.capture('printf more; sh -c exit\ 1')).to eq('more')
56
+ expect(Cmd.capture('printf more && sh -c "exit 1"')).to eq('more')
54
57
  expect($CHILD_STATUS.exitstatus).to eq(1)
55
58
 
56
- expect(Cmd.capture('sh -c exit\ 66')).to eq('')
59
+ expect(Cmd.capture('sh -c "exit 66"')).to eq('')
57
60
  expect($CHILD_STATUS.exitstatus).to eq(66)
58
61
  end
59
62
 
60
63
  it 'raises SignalException if process terminates after signal' do
64
+ skip 'signals are not supported' unless signals_supported?
61
65
  expect_int_exception do
62
66
  Cmd.capture('kill -s INT $$')
63
67
  end
@@ -6,7 +6,7 @@ describe ImageOptim::Config do
6
6
  stub_const('IOConfig', ImageOptim::Config)
7
7
  end
8
8
 
9
- describe :assert_no_unused_options! do
9
+ describe '#assert_no_unused_options!' do
10
10
  before do
11
11
  allow(IOConfig).to receive(:read_options).and_return({})
12
12
  end
@@ -24,7 +24,7 @@ describe ImageOptim::Config do
24
24
  end
25
25
  end
26
26
 
27
- describe :nice do
27
+ describe '#nice' do
28
28
  before do
29
29
  allow(IOConfig).to receive(:read_options).and_return({})
30
30
  end
@@ -45,7 +45,7 @@ describe ImageOptim::Config do
45
45
  end
46
46
  end
47
47
 
48
- describe :threads do
48
+ describe '#threads' do
49
49
  before do
50
50
  allow(IOConfig).to receive(:read_options).and_return({})
51
51
  end
@@ -67,17 +67,40 @@ describe ImageOptim::Config do
67
67
  end
68
68
  end
69
69
 
70
- describe :for_worker do
70
+ describe '#cache_dir' do
71
+ before do
72
+ allow(IOConfig).to receive(:read_options).and_return({})
73
+ end
74
+
75
+ it 'is nil by default' do
76
+ config = IOConfig.new({})
77
+ expect(config.cache_dir).to be nil
78
+ end
79
+
80
+ it 'is nil if set to the empty string' do
81
+ config = IOConfig.new(:cache_dir => '')
82
+ expect(config.cache_dir).to be nil
83
+ end
84
+ end
85
+
86
+ describe '#cache_worker_digests' do
87
+ before do
88
+ allow(IOConfig).to receive(:read_options).and_return({})
89
+ end
90
+
91
+ it 'is false by default' do
92
+ config = IOConfig.new({})
93
+ expect(config.cache_worker_digests).to be false
94
+ end
95
+ end
96
+
97
+ describe '#for_worker' do
71
98
  before do
72
99
  allow(IOConfig).to receive(:read_options).and_return({})
73
100
  stub_const('Abc', Class.new do
74
101
  def self.bin_sym
75
102
  :abc
76
103
  end
77
-
78
- def image_formats
79
- []
80
- end
81
104
  end)
82
105
  end
83
106
 
@@ -104,7 +127,7 @@ describe ImageOptim::Config do
104
127
  end
105
128
  end
106
129
 
107
- describe 'config' do
130
+ describe '#initialize' do
108
131
  it 'reads options from default locations' do
109
132
  expect(IOConfig).to receive(:read_options).
110
133
  with(IOConfig::GLOBAL_PATH).and_return(:a => 1, :b => 2, :c => 3)
@@ -150,68 +173,66 @@ describe ImageOptim::Config do
150
173
  end
151
174
  end
152
175
 
153
- describe 'class methods' do
154
- describe :read_options do
155
- let(:path){ double(:path) }
156
- let(:full_path){ double(:full_path) }
157
-
158
- it 'warns if expand path fails' do
159
- expect(IOConfig).to receive(:warn)
160
- expect(File).to receive(:expand_path).
161
- with(path).and_raise(ArgumentError)
162
- expect(File).not_to receive(:file?)
163
-
164
- expect(IOConfig.read_options(path)).to eq({})
165
- end
166
-
167
- it 'returns empty hash if path is not a file' do
168
- expect(IOConfig).not_to receive(:warn)
169
- expect(File).to receive(:expand_path).
170
- with(path).and_return(full_path)
171
- expect(File).to receive(:file?).
172
- with(full_path).and_return(false)
173
-
174
- expect(IOConfig.read_options(path)).to eq({})
175
- end
176
-
177
- it 'returns hash with deep symbolised keys from reader' do
178
- stringified = {'config' => {'this' => true}}
179
- symbolized = {:config => {:this => true}}
180
-
181
- expect(IOConfig).not_to receive(:warn)
182
- expect(File).to receive(:expand_path).
183
- with(path).and_return(full_path)
184
- expect(File).to receive(:file?).
185
- with(full_path).and_return(true)
186
- expect(YAML).to receive(:load_file).
187
- with(full_path).and_return(stringified)
188
-
189
- expect(IOConfig.read_options(path)).to eq(symbolized)
190
- end
191
-
192
- it 'warns and returns an empty hash if reader returns non hash' do
193
- expect(IOConfig).to receive(:warn)
194
- expect(File).to receive(:expand_path).
195
- with(path).and_return(full_path)
196
- expect(File).to receive(:file?).
197
- with(full_path).and_return(true)
198
- expect(YAML).to receive(:load_file).
199
- with(full_path).and_return([:config])
200
-
201
- expect(IOConfig.read_options(path)).to eq({})
202
- end
203
-
204
- it 'warns and returns an empty hash if reader raises exception' do
205
- expect(IOConfig).to receive(:warn)
206
- expect(File).to receive(:expand_path).
207
- with(path).and_return(full_path)
208
- expect(File).to receive(:file?).
209
- with(full_path).and_return(true)
210
- expect(YAML).to receive(:load_file).
211
- with(full_path).and_raise
212
-
213
- expect(IOConfig.read_options(path)).to eq({})
214
- end
176
+ describe '.read_options' do
177
+ let(:path){ double(:path) }
178
+ let(:full_path){ double(:full_path) }
179
+
180
+ it 'warns if expand path fails' do
181
+ expect(IOConfig).to receive(:warn)
182
+ expect(File).to receive(:expand_path).
183
+ with(path).and_raise(ArgumentError)
184
+ expect(File).not_to receive(:size?)
185
+
186
+ expect(IOConfig.read_options(path)).to eq({})
187
+ end
188
+
189
+ it 'returns empty hash if path is not a file or is an empty file' do
190
+ expect(IOConfig).not_to receive(:warn)
191
+ expect(File).to receive(:expand_path).
192
+ with(path).and_return(full_path)
193
+ expect(File).to receive(:size?).
194
+ with(full_path).and_return(false)
195
+
196
+ expect(IOConfig.read_options(path)).to eq({})
197
+ end
198
+
199
+ it 'returns hash with deep symbolised keys from reader' do
200
+ stringified = {'config' => {'this' => true}}
201
+ symbolized = {:config => {:this => true}}
202
+
203
+ expect(IOConfig).not_to receive(:warn)
204
+ expect(File).to receive(:expand_path).
205
+ with(path).and_return(full_path)
206
+ expect(File).to receive(:size?).
207
+ with(full_path).and_return(true)
208
+ expect(YAML).to receive(:load_file).
209
+ with(full_path).and_return(stringified)
210
+
211
+ expect(IOConfig.read_options(path)).to eq(symbolized)
212
+ end
213
+
214
+ it 'warns and returns an empty hash if reader returns non hash' do
215
+ expect(IOConfig).to receive(:warn)
216
+ expect(File).to receive(:expand_path).
217
+ with(path).and_return(full_path)
218
+ expect(File).to receive(:size?).
219
+ with(full_path).and_return(true)
220
+ expect(YAML).to receive(:load_file).
221
+ with(full_path).and_return([:config])
222
+
223
+ expect(IOConfig.read_options(path)).to eq({})
224
+ end
225
+
226
+ it 'warns and returns an empty hash if reader raises exception' do
227
+ expect(IOConfig).to receive(:warn)
228
+ expect(File).to receive(:expand_path).
229
+ with(path).and_return(full_path)
230
+ expect(File).to receive(:size?).
231
+ with(full_path).and_return(true)
232
+ expect(YAML).to receive(:load_file).
233
+ with(full_path).and_raise
234
+
235
+ expect(IOConfig.read_options(path)).to eq({})
215
236
  end
216
237
  end
217
238
  end