image_optim 0.22.1 → 0.23.0

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 (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