image_optim 0.27.1 → 0.31.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 (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