image_optim 0.27.1 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.appveyor.yml +2 -0
  3. data/.github/workflows/check.yml +59 -0
  4. data/.pre-commit-hooks.yaml +9 -0
  5. data/.rubocop.yml +6 -3
  6. data/CHANGELOG.markdown +22 -0
  7. data/CONTRIBUTING.markdown +5 -2
  8. data/Gemfile +1 -7
  9. data/LICENSE.txt +1 -1
  10. data/README.markdown +21 -10
  11. data/Vagrantfile +1 -1
  12. data/image_optim.gemspec +7 -4
  13. data/lib/image_optim/bin_resolver/bin.rb +10 -9
  14. data/lib/image_optim/cache.rb +6 -0
  15. data/lib/image_optim/cmd.rb +45 -6
  16. data/lib/image_optim/config.rb +14 -8
  17. data/lib/image_optim/elapsed_time.rb +26 -0
  18. data/lib/image_optim/errors.rb +9 -0
  19. data/lib/image_optim/path.rb +1 -1
  20. data/lib/image_optim/runner/option_parser.rb +24 -18
  21. data/lib/image_optim/runner.rb +1 -1
  22. data/lib/image_optim/timer.rb +25 -0
  23. data/lib/image_optim/worker/advpng.rb +7 -7
  24. data/lib/image_optim/worker/gifsicle.rb +11 -11
  25. data/lib/image_optim/worker/jhead.rb +2 -2
  26. data/lib/image_optim/worker/jpegoptim.rb +13 -11
  27. data/lib/image_optim/worker/jpegrecompress.rb +17 -2
  28. data/lib/image_optim/worker/jpegtran.rb +4 -4
  29. data/lib/image_optim/worker/optipng.rb +7 -7
  30. data/lib/image_optim/worker/oxipng.rb +53 -0
  31. data/lib/image_optim/worker/pngcrush.rb +6 -6
  32. data/lib/image_optim/worker/pngout.rb +7 -7
  33. data/lib/image_optim/worker/pngquant.rb +10 -9
  34. data/lib/image_optim/worker/svgo.rb +2 -2
  35. data/lib/image_optim/worker.rb +32 -29
  36. data/lib/image_optim.rb +16 -10
  37. data/script/update_worker_options_in_readme +2 -2
  38. data/script/worker_analysis +16 -18
  39. data/spec/image_optim/bin_resolver_spec.rb +5 -5
  40. data/spec/image_optim/cache_path_spec.rb +7 -10
  41. data/spec/image_optim/cache_spec.rb +8 -8
  42. data/spec/image_optim/cmd_spec.rb +64 -6
  43. data/spec/image_optim/config_spec.rb +36 -20
  44. data/spec/image_optim/elapsed_time_spec.rb +14 -0
  45. data/spec/image_optim/handler_spec.rb +1 -1
  46. data/spec/image_optim/hash_helpers_spec.rb +18 -18
  47. data/spec/image_optim/option_definition_spec.rb +6 -6
  48. data/spec/image_optim/path_spec.rb +8 -11
  49. data/spec/image_optim/runner/option_parser_spec.rb +4 -4
  50. data/spec/image_optim/timer_spec.rb +32 -0
  51. data/spec/image_optim/worker/jpegrecompress_spec.rb +32 -0
  52. data/spec/image_optim/worker/optipng_spec.rb +11 -11
  53. data/spec/image_optim/worker/oxipng_spec.rb +89 -0
  54. data/spec/image_optim/worker/pngquant_spec.rb +5 -5
  55. data/spec/image_optim/worker_spec.rb +17 -17
  56. data/spec/image_optim_spec.rb +47 -10
  57. data/spec/images/invisiblepixels/generate +1 -1
  58. data/spec/spec_helper.rb +18 -17
  59. metadata +36 -15
  60. data/.travis.yml +0 -49
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'image_optim/cmd'
5
5
  require 'image_optim/configuration_error'
6
+ require 'image_optim/elapsed_time'
6
7
  require 'image_optim/path'
7
8
  require 'image_optim/worker/class_methods'
8
9
  require 'shellwords'
@@ -41,7 +42,7 @@ class ImageOptim
41
42
 
42
43
  # Optimize image at src, output at dst, must be overriden in subclass
43
44
  # return true on success
44
- def optimize(_src, _dst)
45
+ def optimize(_src, _dst, options = {})
45
46
  fail NotImplementedError, "implement method optimize in #{self.class}"
46
47
  end
47
48
 
@@ -104,7 +105,7 @@ class ImageOptim
104
105
  return if unknown_options.empty?
105
106
 
106
107
  fail ConfigurationError, "unknown options #{unknown_options.inspect} "\
107
- "for #{self}"
108
+ "for #{self}"
108
109
  end
109
110
 
110
111
  # Forward bin resolving to image_optim
@@ -117,47 +118,49 @@ class ImageOptim
117
118
  def wrap_resolver_error_message(message)
118
119
  name = self.class.bin_sym
119
120
  "#{name} worker: #{message}; please provide proper binary or "\
120
- "disable this worker (--no-#{name} argument or "\
121
- "`:#{name} => false` through options)"
121
+ "disable this worker (--no-#{name} argument or "\
122
+ "`:#{name} => false` through options)"
122
123
  end
123
124
 
124
125
  # Run command setting priority and hiding output
125
- def execute(bin, *arguments)
126
+ def execute(bin, arguments, options)
126
127
  resolve_bin!(bin)
127
128
 
128
129
  cmd_args = [bin, *arguments].map(&:to_s)
129
130
 
130
- start = Time.now
131
-
132
- success = run_command(cmd_args)
133
-
134
131
  if @image_optim.verbose
135
- seconds = Time.now - start
136
- $stderr << "#{success ? '✓' : '✗'} #{seconds}s #{cmd_args.shelljoin}\n"
132
+ run_command_verbose(cmd_args, options)
133
+ else
134
+ run_command(cmd_args, options)
137
135
  end
138
-
139
- success
140
136
  end
141
137
 
142
138
  # Run command defining environment, setting nice level, removing output and
143
139
  # reraising signal exception
144
- def run_command(cmd_args)
145
- args = if RUBY_VERSION < '1.9' || defined?(JRUBY_VERSION)
146
- %W[
147
- env PATH=#{@image_optim.env_path.shellescape}
148
- nice -n #{@image_optim.nice}
149
- #{cmd_args.shelljoin}
150
- > #{Path::NULL} 2>&1
151
- ].join(' ')
152
- else
153
- [
154
- {'PATH' => @image_optim.env_path},
155
- %W[nice -n #{@image_optim.nice}],
156
- cmd_args,
157
- {:out => Path::NULL, :err => Path::NULL},
158
- ].flatten
159
- end
140
+ def run_command(cmd_args, options)
141
+ args = [
142
+ {'PATH' => @image_optim.env_path},
143
+ *%W[nice -n #{@image_optim.nice}],
144
+ *cmd_args,
145
+ options.merge(out: Path::NULL, err: Path::NULL),
146
+ ]
160
147
  Cmd.run(*args)
161
148
  end
149
+
150
+ # Wrap run_command and output status, elapsed time and command
151
+ def run_command_verbose(cmd_args, options)
152
+ start = ElapsedTime.now
153
+
154
+ begin
155
+ success = run_command(cmd_args, options)
156
+ status = success ? '✓' : '✗'
157
+ success
158
+ rescue Errors::TimeoutExceeded
159
+ status = 'timeout'
160
+ raise
161
+ ensure
162
+ $stderr << format("%s %.1fs %s\n", status, ElapsedTime.now - start, cmd_args.shelljoin)
163
+ end
164
+ end
162
165
  end
163
166
  end
data/lib/image_optim.rb CHANGED
@@ -3,16 +3,18 @@
3
3
  require 'image_optim/bin_resolver'
4
4
  require 'image_optim/cache'
5
5
  require 'image_optim/config'
6
+ require 'image_optim/errors'
6
7
  require 'image_optim/handler'
7
8
  require 'image_optim/image_meta'
8
9
  require 'image_optim/optimized_path'
9
10
  require 'image_optim/path'
11
+ require 'image_optim/timer'
10
12
  require 'image_optim/worker'
11
13
  require 'in_threads'
12
14
  require 'shellwords'
13
15
 
14
16
  %w[
15
- pngcrush pngout advpng optipng pngquant
17
+ pngcrush pngout advpng optipng pngquant oxipng
16
18
  jhead jpegoptim jpegrecompress jpegtran
17
19
  gifsicle
18
20
  svgo
@@ -46,6 +48,9 @@ class ImageOptim
46
48
  # Cache worker digests
47
49
  attr_reader :cache_worker_digests
48
50
 
51
+ # Timeout in seconds for each image
52
+ attr_reader :timeout
53
+
49
54
  # Initialize workers, specify options using worker underscored name:
50
55
  #
51
56
  # pass false to disable worker
@@ -78,6 +83,7 @@ class ImageOptim
78
83
  allow_lossy
79
84
  cache_dir
80
85
  cache_worker_digests
86
+ timeout
81
87
  ].each do |name|
82
88
  instance_variable_set(:"@#{name}", config.send(name))
83
89
  $stderr << "#{name}: #{send(name)}\n" if verbose
@@ -110,11 +116,17 @@ class ImageOptim
110
116
  return unless (workers = workers_for_image(original))
111
117
 
112
118
  optimized = @cache.fetch(original) do
119
+ timer = timeout && Timer.new(timeout)
120
+
113
121
  Handler.for(original) do |handler|
114
- workers.each do |worker|
115
- handler.process do |src, dst|
116
- worker.optimize(src, dst)
122
+ begin
123
+ workers.each do |worker|
124
+ handler.process do |src, dst|
125
+ worker.optimize(src, dst, timeout: timer)
126
+ end
117
127
  end
128
+ rescue Errors::TimeoutExceeded
129
+ handler.result
118
130
  end
119
131
  end
120
132
  end
@@ -188,12 +200,6 @@ class ImageOptim
188
200
  optimize_image_method?(method) || super
189
201
  end
190
202
 
191
- if RUBY_VERSION < '1.9'
192
- def respond_to?(method, include_private = false)
193
- optimize_image_method?(method) || super
194
- end
195
- end
196
-
197
203
  # Version of image_optim gem spec loaded
198
204
  def version
199
205
  Gem.loaded_specs['image_optim'].version.to_s
@@ -18,7 +18,7 @@ def write_worker_options(io, klass)
18
18
  klass.option_definitions.each do |option_definition|
19
19
  line = "* `:#{option_definition.name}` — #{option_definition.description}"
20
20
  unless line['(defaults']
21
- line << " *(defaults to #{option_definition.default_description})*"
21
+ line += " *(defaults to #{option_definition.default_description})*"
22
22
  end
23
23
  io.puts line
24
24
  end
@@ -29,7 +29,7 @@ end
29
29
  def write_marked(io)
30
30
  io.puts BEGIN_MARKER
31
31
  io.puts '<!-- markdown for worker options is generated by '\
32
- "`#{Pathname($PROGRAM_NAME).cleanpath}` -->"
32
+ "`#{Pathname($PROGRAM_NAME).cleanpath}` -->"
33
33
  io.puts
34
34
 
35
35
  ImageOptim::Worker.klasses.sort_by(&:name).each do |klass|
@@ -358,20 +358,18 @@ class Analyser
358
358
  end
359
359
 
360
360
  def flatten_animation(image)
361
- run_cache[:flatten][image.digest] ||= begin
362
- if image.image_format == :gif
363
- flattened = image.temp_path
364
- Cmd.run(*%W[
365
- convert
366
- #{image.image_format}:#{image}
367
- -coalesce
368
- -append
369
- #{image.image_format}:#{flattened}
370
- ]) || fail("failed flattening of #{image}")
371
- flattened
372
- else
373
- image
374
- end
361
+ run_cache[:flatten][image.digest] ||= if image.image_format == :gif
362
+ flattened = image.temp_path
363
+ Cmd.run(*%W[
364
+ convert
365
+ #{image.image_format}:#{image}
366
+ -coalesce
367
+ -append
368
+ #{image.image_format}:#{flattened}
369
+ ]) || fail("failed flattening of #{image}")
370
+ flattened
371
+ else
372
+ image
375
373
  end
376
374
  end
377
375
 
@@ -527,10 +525,10 @@ class Analyser
527
525
  stats = Stats.new('all', by_format[format], worker_ids2names)
528
526
  path = FSPath("#{DIR}/#{basenames[format]}")
529
527
  model = {
530
- :stats_format => format,
531
- :stats => stats,
532
- :format_links => basenames,
533
- :template_dir => template_path.dirname.relative_path_from(path.dirname),
528
+ stats_format: format,
529
+ stats: stats,
530
+ format_links: basenames,
531
+ template_dir: template_path.dirname.relative_path_from(path.dirname),
534
532
  }
535
533
  html = template.result(OpenStruct.new(model).instance_eval{ binding })
536
534
  path.write(html)
@@ -19,7 +19,7 @@ describe ImageOptim::BinResolver do
19
19
  allow(ENV).to receive(:[]).and_call_original
20
20
  end
21
21
 
22
- let(:image_optim){ double(:image_optim, :verbose => false, :pack => false) }
22
+ let(:image_optim){ double(:image_optim, verbose: false, pack: false) }
23
23
  let(:resolver){ BinResolver.new(image_optim) }
24
24
 
25
25
  describe '#full_path' do
@@ -130,7 +130,7 @@ describe ImageOptim::BinResolver do
130
130
  it 'resolves bin specified in ENV' do
131
131
  path = 'bin/the_optimizer'
132
132
  stub_env 'THE_OPTIMIZER_BIN', path
133
- tmpdir = double(:tmpdir, :to_str => 'tmpdir')
133
+ tmpdir = double(:tmpdir, to_str: 'tmpdir')
134
134
  symlink = double(:symlink)
135
135
 
136
136
  full_path = File.expand_path(path)
@@ -180,9 +180,9 @@ describe ImageOptim::BinResolver do
180
180
  stub_env 'THE_OPTIMIZER_BIN', path
181
181
  expect(FSPath).not_to receive(:temp_dir)
182
182
  expect(resolver).not_to receive(:at_exit)
183
- allow(File).to receive_messages(:exist? => exist?,
184
- :file? => file?,
185
- :executable? => executable?)
183
+ allow(File).to receive_messages(exist?: exist?,
184
+ file?: file?,
185
+ executable?: executable?)
186
186
  end
187
187
 
188
188
  after do
@@ -5,8 +5,6 @@ require 'image_optim/cache_path'
5
5
  require 'tempfile'
6
6
 
7
7
  describe ImageOptim::CachePath do
8
- include CapabilityCheckHelpers
9
-
10
8
  before do
11
9
  stub_const('Path', ImageOptim::Path)
12
10
  stub_const('CachePath', ImageOptim::CachePath)
@@ -32,8 +30,7 @@ describe ImageOptim::CachePath do
32
30
  expect(src).to exist
33
31
  end
34
32
 
35
- it 'preserves attributes of destination file' do
36
- skip 'full file modes are not support' unless any_file_modes_allowed?
33
+ it 'preserves attributes of destination file', skip: SkipConditions[:any_file_mode_allowed] do
37
34
  mode = 0o666
38
35
 
39
36
  dst.chmod(mode)
@@ -45,22 +42,22 @@ describe ImageOptim::CachePath do
45
42
  end
46
43
 
47
44
  it 'does not preserve mtime of destination file' do
48
- time = src.mtime
45
+ time = src.mtime - 1000
46
+ dst.utime(time, time)
49
47
 
50
- dst.utime(time - 1000, time - 1000)
48
+ time = dst.mtime
51
49
 
52
50
  src.replace(dst)
53
51
 
54
- expect(dst.mtime).to be >= time
52
+ expect(dst.mtime).to_not eq(time)
55
53
  end
56
54
 
57
- it 'changes inode of destination' do
58
- skip 'inodes are not supported' unless inodes_supported?
55
+ it 'changes inode of destination', skip: SkipConditions[:inodes_support] do
59
56
  expect{ src.replace(dst) }.to change{ dst.stat.ino }
60
57
  end
61
58
 
62
59
  it 'is using temporary file with .tmp extension' do
63
- expect(src).to receive(:copy).with(having_attributes(:extname => '.tmp')).at_least(:once)
60
+ expect(src).to receive(:copy).with(having_attributes(extname: '.tmp')).at_least(:once)
64
61
 
65
62
  src.replace(dst)
66
63
  end
@@ -11,7 +11,7 @@ describe ImageOptim::Cache do
11
11
  stub_const('CachePath', ImageOptim::CachePath)
12
12
  end
13
13
 
14
- let(:tmp_file){ double('/somewhere/tmp/foo/bar', :rename => 0) }
14
+ let(:tmp_file){ double('/somewhere/tmp/foo/bar', rename: 0) }
15
15
 
16
16
  let(:cache_dir) do
17
17
  dir = '/somewhere/cache'
@@ -24,7 +24,7 @@ describe ImageOptim::Cache do
24
24
  end
25
25
 
26
26
  let(:original) do
27
- original = double('/somewhere/original', :image_format => :ext)
27
+ original = double('/somewhere/original', image_format: :ext)
28
28
  allow(Digest::SHA1).to receive(:file).with(original) do
29
29
  Digest::SHA1.new << 'some content!'
30
30
  end
@@ -32,7 +32,7 @@ describe ImageOptim::Cache do
32
32
  end
33
33
 
34
34
  let(:optimized) do
35
- double('/somewhere/optimized', :format => :ext, :basename => 'optimized')
35
+ double('/somewhere/optimized', format: :ext, basename: 'optimized')
36
36
  end
37
37
 
38
38
  let(:cached) do
@@ -45,7 +45,7 @@ describe ImageOptim::Cache do
45
45
 
46
46
  context 'when cache is disabled (default)' do
47
47
  let(:image_optim) do
48
- double(:image_optim, :cache_dir => nil, :cache_worker_digests => false)
48
+ double(:image_optim, cache_dir: nil, cache_worker_digests: false, timeout: nil)
49
49
  end
50
50
  let(:cache){ Cache.new(image_optim, double) }
51
51
 
@@ -102,7 +102,7 @@ describe ImageOptim::Cache do
102
102
  expect(FileUtils).not_to receive(:mv)
103
103
  expect(File).not_to receive(:rename)
104
104
 
105
- expect(cache.fetch(original){}).to eq(cached)
105
+ expect(cache.fetch(original){ nil }).to eq(cached)
106
106
  end
107
107
 
108
108
  it 'returns nil when file is already optimized' do
@@ -122,7 +122,7 @@ describe ImageOptim::Cache do
122
122
  context 'when cache is enabled (without worker digests)' do
123
123
  let(:image_optim) do
124
124
  double(:image_optim,
125
- :cache_dir => cache_dir, :cache_worker_digests => false)
125
+ cache_dir: cache_dir, cache_worker_digests: false, timeout: nil)
126
126
  end
127
127
  let(:cache) do
128
128
  cache = Cache.new(image_optim, {})
@@ -144,8 +144,8 @@ describe ImageOptim::Cache do
144
144
  context 'when cache is enabled (with worker digests)' do
145
145
  let(:image_optim) do
146
146
  double(:image_optim,
147
- :cache_dir => cache_dir,
148
- :cache_worker_digests => true)
147
+ cache_dir: cache_dir,
148
+ cache_worker_digests: true, timeout: nil)
149
149
  end
150
150
  let(:cache) do
151
151
  cache = Cache.new(image_optim, {})
@@ -2,10 +2,9 @@
2
2
 
3
3
  require 'spec_helper'
4
4
  require 'image_optim/cmd'
5
+ require 'image_optim/timer'
5
6
 
6
7
  describe ImageOptim::Cmd do
7
- include CapabilityCheckHelpers
8
-
9
8
  before do
10
9
  stub_const('Cmd', ImageOptim::Cmd)
11
10
  end
@@ -35,12 +34,72 @@ describe ImageOptim::Cmd do
35
34
  expect($CHILD_STATUS.exitstatus).to eq(66)
36
35
  end
37
36
 
38
- it 'raises SignalException if process terminates after signal' do
39
- skip 'signals are not supported' unless signals_supported?
37
+ it 'raises SignalException if process terminates after signal', skip: SkipConditions[:signals_support] do
40
38
  expect_int_exception do
41
39
  Cmd.run('kill -s INT $$')
42
40
  end
43
41
  end
42
+
43
+ context 'with timeout' do
44
+ it 'returns process success status' do
45
+ expect(Cmd.run('sh -c "exit 0"', timeout: 1)).to eq(true)
46
+
47
+ expect(Cmd.run('sh -c "exit 1"', timeout: 1)).to eq(false)
48
+
49
+ expect(Cmd.run('sh -c "exit 66"', timeout: 1)).to eq(false)
50
+ end
51
+
52
+ it 'returns process success status when timeout is instance of ImageOptim::Timer' do
53
+ timeout = ImageOptim::Timer.new(1.0)
54
+ expect(Cmd.run('sh -c "exit 0"', timeout: timeout)).to eq(true)
55
+ end
56
+
57
+ it 'raises SignalException if process terminates after signal', skip: SkipConditions[:signals_support] do
58
+ expect_int_exception do
59
+ Cmd.run('kill -s INT $$', timeout: 1)
60
+ end
61
+ end
62
+
63
+ it 'raises TimeoutExceeded if process does not exit until timeout' do
64
+ expect do
65
+ Cmd.run('sleep 10', timeout: 0)
66
+ end.to raise_error(ImageOptim::Errors::TimeoutExceeded)
67
+ end
68
+
69
+ it 'does not leave zombie threads' do
70
+ expect do
71
+ begin
72
+ Cmd.run('sleep 10', timeout: 0)
73
+ rescue ImageOptim::Errors::TimeoutExceeded
74
+ # noop
75
+ end
76
+ end.not_to change{ Thread.list }
77
+ end
78
+
79
+ it 'receives TERM', skip: SkipConditions[:signals_support] do
80
+ waiter = double
81
+ allow(Process).to receive(:detach).once{ |pid| @pid = pid; waiter }
82
+ allow(waiter).to receive(:join){ sleep 0.1; nil }
83
+
84
+ expect do
85
+ Cmd.run('sleep 5', timeout: 0.1)
86
+ end.to raise_error(ImageOptim::Errors::TimeoutExceeded)
87
+
88
+ expect(Process.wait2(@pid).last.termsig).to eq(Signal.list['TERM'])
89
+ end
90
+
91
+ it 'receives KILL if it does not react on TERM', skip: SkipConditions[:signals_support] do
92
+ waiter = double
93
+ allow(Process).to receive(:detach).once{ |pid| @pid = pid; waiter }
94
+ allow(waiter).to receive(:join){ sleep 0.1; nil }
95
+
96
+ expect do
97
+ Cmd.run('trap "" TERM; sleep 5', timeout: 0.1)
98
+ end.to raise_error(ImageOptim::Errors::TimeoutExceeded)
99
+
100
+ expect(Process.wait2(@pid).last.termsig).to eq(Signal.list['KILL'])
101
+ end
102
+ end
44
103
  end
45
104
 
46
105
  describe '.capture' do
@@ -62,8 +121,7 @@ describe ImageOptim::Cmd do
62
121
  expect($CHILD_STATUS.exitstatus).to eq(66)
63
122
  end
64
123
 
65
- it 'raises SignalException if process terminates after signal' do
66
- skip 'signals are not supported' unless signals_supported?
124
+ it 'raises SignalException if process terminates after signal', skip: SkipConditions[:signals_support] do
67
125
  expect_int_exception do
68
126
  Cmd.capture('kill -s INT $$')
69
127
  end
@@ -19,7 +19,7 @@ describe ImageOptim::Config do
19
19
  end
20
20
 
21
21
  it 'raises when there are unused options' do
22
- config = IOConfig.new(:unused => true)
22
+ config = IOConfig.new(unused: true)
23
23
  expect do
24
24
  config.assert_no_unused_options!
25
25
  end.to raise_error(ImageOptim::ConfigurationError)
@@ -37,12 +37,12 @@ describe ImageOptim::Config do
37
37
  end
38
38
 
39
39
  it 'is 0 if disabled' do
40
- config = IOConfig.new(:nice => false)
40
+ config = IOConfig.new(nice: false)
41
41
  expect(config.nice).to eq(0)
42
42
  end
43
43
 
44
44
  it 'converts value to number' do
45
- config = IOConfig.new(:nice => '13')
45
+ config = IOConfig.new(nice: '13')
46
46
  expect(config.nice).to eq(13)
47
47
  end
48
48
  end
@@ -59,16 +59,32 @@ describe ImageOptim::Config do
59
59
  end
60
60
 
61
61
  it 'is 1 if disabled' do
62
- config = IOConfig.new(:threads => false)
62
+ config = IOConfig.new(threads: false)
63
63
  expect(config.threads).to eq(1)
64
64
  end
65
65
 
66
66
  it 'converts value to number' do
67
- config = IOConfig.new(:threads => '616')
67
+ config = IOConfig.new(threads: '616')
68
68
  expect(config.threads).to eq(616)
69
69
  end
70
70
  end
71
71
 
72
+ describe '#timeout' do
73
+ before do
74
+ allow(IOConfig).to receive(:read_options).and_return({})
75
+ end
76
+
77
+ it 'is nil by default' do
78
+ config = IOConfig.new({})
79
+ expect(config.timeout).to eq(nil)
80
+ end
81
+
82
+ it 'converts value to a float' do
83
+ config = IOConfig.new(timeout: '15.1')
84
+ expect(config.timeout).to eq(15.1)
85
+ end
86
+ end
87
+
72
88
  describe '#cache_dir' do
73
89
  before do
74
90
  allow(IOConfig).to receive(:read_options).and_return({})
@@ -80,7 +96,7 @@ describe ImageOptim::Config do
80
96
  end
81
97
 
82
98
  it 'is nil if set to the empty string' do
83
- config = IOConfig.new(:cache_dir => '')
99
+ config = IOConfig.new(cache_dir: '')
84
100
  expect(config.cache_dir).to be nil
85
101
  end
86
102
  end
@@ -112,17 +128,17 @@ describe ImageOptim::Config do
112
128
  end
113
129
 
114
130
  it 'returns passed hash' do
115
- config = IOConfig.new(:abc => {:option => true})
116
- expect(config.for_worker(Abc)).to eq(:option => true)
131
+ config = IOConfig.new(abc: {option: true})
132
+ expect(config.for_worker(Abc)).to eq(option: true)
117
133
  end
118
134
 
119
135
  it 'returns {:disable => true} for false' do
120
- config = IOConfig.new(:abc => false)
121
- expect(config.for_worker(Abc)).to eq(:disable => true)
136
+ config = IOConfig.new(abc: false)
137
+ expect(config.for_worker(Abc)).to eq(disable: true)
122
138
  end
123
139
 
124
140
  it 'raises on unknown option' do
125
- config = IOConfig.new(:abc => 13)
141
+ config = IOConfig.new(abc: 13)
126
142
  expect do
127
143
  config.for_worker(Abc)
128
144
  end.to raise_error(ImageOptim::ConfigurationError)
@@ -132,11 +148,11 @@ describe ImageOptim::Config do
132
148
  describe '#initialize' do
133
149
  it 'reads options from default locations' do
134
150
  expect(IOConfig).to receive(:read_options).
135
- with(IOConfig::GLOBAL_PATH).and_return(:a => 1, :b => 2, :c => 3)
151
+ with(IOConfig::GLOBAL_PATH).and_return(a: 1, b: 2, c: 3)
136
152
  expect(IOConfig).to receive(:read_options).
137
- with(IOConfig::LOCAL_PATH).and_return(:a => 10, :b => 20)
153
+ with(IOConfig::LOCAL_PATH).and_return(a: 10, b: 20)
138
154
 
139
- config = IOConfig.new(:a => 100)
155
+ config = IOConfig.new(a: 100)
140
156
  expect(config.get!(:a)).to eq(100)
141
157
  expect(config.get!(:b)).to eq(20)
142
158
  expect(config.get!(:c)).to eq(3)
@@ -146,17 +162,17 @@ describe ImageOptim::Config do
146
162
  it 'does not read options with empty config_paths' do
147
163
  expect(IOConfig).not_to receive(:read_options)
148
164
 
149
- config = IOConfig.new(:config_paths => [])
165
+ config = IOConfig.new(config_paths: [])
150
166
  config.assert_no_unused_options!
151
167
  end
152
168
 
153
169
  it 'reads options from specified paths' do
154
170
  expect(IOConfig).to receive(:read_options).
155
- with('/etc/image_optim.yml').and_return(:a => 1, :b => 2, :c => 3)
171
+ with('/etc/image_optim.yml').and_return(a: 1, b: 2, c: 3)
156
172
  expect(IOConfig).to receive(:read_options).
157
- with('config/image_optim.yml').and_return(:a => 10, :b => 20)
173
+ with('config/image_optim.yml').and_return(a: 10, b: 20)
158
174
 
159
- config = IOConfig.new(:a => 100, :config_paths => %w[
175
+ config = IOConfig.new(a: 100, config_paths: %w[
160
176
  /etc/image_optim.yml
161
177
  config/image_optim.yml
162
178
  ])
@@ -170,7 +186,7 @@ describe ImageOptim::Config do
170
186
  expect(IOConfig).to receive(:read_options).
171
187
  with('config/image_optim.yml').and_return({})
172
188
 
173
- config = IOConfig.new(:config_paths => 'config/image_optim.yml')
189
+ config = IOConfig.new(config_paths: 'config/image_optim.yml')
174
190
  config.assert_no_unused_options!
175
191
  end
176
192
  end
@@ -200,7 +216,7 @@ describe ImageOptim::Config do
200
216
 
201
217
  it 'returns hash with deep symbolised keys from reader' do
202
218
  stringified = {'config' => {'this' => true}}
203
- symbolized = {:config => {:this => true}}
219
+ symbolized = {config: {this: true}}
204
220
 
205
221
  expect(IOConfig).not_to receive(:warn)
206
222
  expect(File).to receive(:expand_path).
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'image_optim/elapsed_time'
5
+
6
+ describe ImageOptim::ElapsedTime do
7
+ let(:timeout){ 0.01 }
8
+
9
+ describe '.now' do
10
+ it 'returns incrementing value' do
11
+ expect{ sleep timeout }.to change{ described_class.now }.by_at_least(timeout)
12
+ end
13
+ end
14
+ end
@@ -9,7 +9,7 @@ describe ImageOptim::Handler do
9
9
  end
10
10
 
11
11
  it 'uses original as source for first conversion '\
12
- 'and two temp files for further conversions' do
12
+ 'and two temp files for further conversions' do
13
13
  original = double(:original)
14
14
  allow(original).to receive(:respond_to?).with(:temp_path).and_return(true)
15
15