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.
- checksums.yaml +4 -4
- data/.appveyor.yml +2 -0
- data/.github/workflows/check.yml +59 -0
- data/.pre-commit-hooks.yaml +9 -0
- data/.rubocop.yml +6 -3
- data/CHANGELOG.markdown +22 -0
- data/CONTRIBUTING.markdown +5 -2
- data/Gemfile +1 -7
- data/LICENSE.txt +1 -1
- data/README.markdown +21 -10
- data/Vagrantfile +1 -1
- data/image_optim.gemspec +7 -4
- data/lib/image_optim/bin_resolver/bin.rb +10 -9
- data/lib/image_optim/cache.rb +6 -0
- data/lib/image_optim/cmd.rb +45 -6
- data/lib/image_optim/config.rb +14 -8
- data/lib/image_optim/elapsed_time.rb +26 -0
- data/lib/image_optim/errors.rb +9 -0
- data/lib/image_optim/path.rb +1 -1
- data/lib/image_optim/runner/option_parser.rb +24 -18
- data/lib/image_optim/runner.rb +1 -1
- data/lib/image_optim/timer.rb +25 -0
- data/lib/image_optim/worker/advpng.rb +7 -7
- data/lib/image_optim/worker/gifsicle.rb +11 -11
- data/lib/image_optim/worker/jhead.rb +2 -2
- data/lib/image_optim/worker/jpegoptim.rb +13 -11
- data/lib/image_optim/worker/jpegrecompress.rb +17 -2
- data/lib/image_optim/worker/jpegtran.rb +4 -4
- data/lib/image_optim/worker/optipng.rb +7 -7
- data/lib/image_optim/worker/oxipng.rb +53 -0
- data/lib/image_optim/worker/pngcrush.rb +6 -6
- data/lib/image_optim/worker/pngout.rb +7 -7
- data/lib/image_optim/worker/pngquant.rb +10 -9
- data/lib/image_optim/worker/svgo.rb +2 -2
- data/lib/image_optim/worker.rb +32 -29
- data/lib/image_optim.rb +16 -10
- data/script/update_worker_options_in_readme +2 -2
- data/script/worker_analysis +16 -18
- data/spec/image_optim/bin_resolver_spec.rb +5 -5
- data/spec/image_optim/cache_path_spec.rb +7 -10
- data/spec/image_optim/cache_spec.rb +8 -8
- data/spec/image_optim/cmd_spec.rb +64 -6
- data/spec/image_optim/config_spec.rb +36 -20
- data/spec/image_optim/elapsed_time_spec.rb +14 -0
- data/spec/image_optim/handler_spec.rb +1 -1
- data/spec/image_optim/hash_helpers_spec.rb +18 -18
- data/spec/image_optim/option_definition_spec.rb +6 -6
- data/spec/image_optim/path_spec.rb +8 -11
- data/spec/image_optim/runner/option_parser_spec.rb +4 -4
- data/spec/image_optim/timer_spec.rb +32 -0
- data/spec/image_optim/worker/jpegrecompress_spec.rb +32 -0
- data/spec/image_optim/worker/optipng_spec.rb +11 -11
- data/spec/image_optim/worker/oxipng_spec.rb +89 -0
- data/spec/image_optim/worker/pngquant_spec.rb +5 -5
- data/spec/image_optim/worker_spec.rb +17 -17
- data/spec/image_optim_spec.rb +47 -10
- data/spec/images/invisiblepixels/generate +1 -1
- data/spec/spec_helper.rb +18 -17
- metadata +36 -15
- data/.travis.yml +0 -49
data/lib/image_optim/worker.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
121
|
-
|
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,
|
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
|
-
|
136
|
-
|
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 =
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
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
|
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
|
-
|
32
|
+
"`#{Pathname($PROGRAM_NAME).cleanpath}` -->"
|
33
33
|
io.puts
|
34
34
|
|
35
35
|
ImageOptim::Worker.klasses.sort_by(&:name).each do |klass|
|
data/script/worker_analysis
CHANGED
@@ -358,20 +358,18 @@ class Analyser
|
|
358
358
|
end
|
359
359
|
|
360
360
|
def flatten_animation(image)
|
361
|
-
run_cache[:flatten][image.digest] ||=
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
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
|
-
:
|
531
|
-
:
|
532
|
-
:
|
533
|
-
:
|
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, :
|
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, :
|
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(
|
184
|
-
|
185
|
-
|
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
|
-
|
48
|
+
time = dst.mtime
|
51
49
|
|
52
50
|
src.replace(dst)
|
53
51
|
|
54
|
-
expect(dst.mtime).
|
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(:
|
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', :
|
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', :
|
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', :
|
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, :
|
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
|
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
|
-
:
|
148
|
-
:
|
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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
116
|
-
expect(config.for_worker(Abc)).to eq(:
|
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(:
|
121
|
-
expect(config.for_worker(Abc)).to eq(:
|
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(:
|
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(:
|
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(:
|
153
|
+
with(IOConfig::LOCAL_PATH).and_return(a: 10, b: 20)
|
138
154
|
|
139
|
-
config = IOConfig.new(:
|
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(:
|
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(:
|
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(:
|
173
|
+
with('config/image_optim.yml').and_return(a: 10, b: 20)
|
158
174
|
|
159
|
-
config = IOConfig.new(:
|
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(:
|
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 = {:
|
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
|
-
|
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
|
|