image_optim 0.18.0 → 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/.travis.yml +14 -17
- data/CHANGELOG.markdown +10 -0
- data/README.markdown +13 -3
- data/image_optim.gemspec +3 -3
- data/lib/image_optim.rb +16 -31
- data/lib/image_optim/bin_resolver.rb +3 -1
- data/lib/image_optim/bin_resolver/bin.rb +33 -24
- data/lib/image_optim/config.rb +5 -0
- data/lib/image_optim/runner/option_parser.rb +12 -5
- data/lib/image_optim/worker.rb +10 -62
- data/lib/image_optim/worker/class_methods.rb +89 -0
- data/lib/image_optim/worker/gifsicle.rb +25 -2
- data/lib/image_optim/worker/jpegrecompress.rb +44 -0
- data/lib/image_optim/worker/optipng.rb +1 -1
- data/script/update_worker_options_in_readme +6 -7
- data/script/worker_analysis +15 -5
- data/script/worker_analysis.haml +31 -10
- data/spec/image_optim/bin_resolver_spec.rb +52 -20
- data/spec/image_optim/worker_spec.rb +139 -0
- data/spec/image_optim_spec.rb +35 -53
- metadata +9 -7
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'image_optim/bin_resolver'
|
2
|
+
require 'image_optim/option_definition'
|
3
|
+
|
4
|
+
class ImageOptim
|
5
|
+
class Worker
|
6
|
+
# Class methods of ImageOptim::Worker
|
7
|
+
module ClassMethods
|
8
|
+
def self.extended(klass)
|
9
|
+
klass.instance_variable_set(:@klasses, [])
|
10
|
+
end
|
11
|
+
|
12
|
+
# List of available workers
|
13
|
+
def klasses
|
14
|
+
@klasses.to_enum
|
15
|
+
end
|
16
|
+
|
17
|
+
# Remember all classes inheriting from this one
|
18
|
+
def inherited(base)
|
19
|
+
@klasses << base
|
20
|
+
end
|
21
|
+
|
22
|
+
# Underscored class name symbol
|
23
|
+
def bin_sym
|
24
|
+
@underscored_name ||= name.
|
25
|
+
split('::').last. # get last part
|
26
|
+
gsub(/([a-z])([A-Z])/, '\1_\2').downcase. # convert AbcDef to abc_def
|
27
|
+
to_sym
|
28
|
+
end
|
29
|
+
|
30
|
+
def option_definitions
|
31
|
+
@option_definitions ||= []
|
32
|
+
end
|
33
|
+
|
34
|
+
def option(name, default, type, description = nil, &proc)
|
35
|
+
attr_reader name
|
36
|
+
option_definitions <<
|
37
|
+
OptionDefinition.new(name, default, type, description, &proc)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create hash with format mapped to list of workers sorted by run order
|
41
|
+
def create_all_by_format(image_optim, &options_proc)
|
42
|
+
by_format = {}
|
43
|
+
create_all(image_optim, &options_proc).each do |worker|
|
44
|
+
worker.image_formats.each do |format|
|
45
|
+
by_format[format] ||= []
|
46
|
+
by_format[format] << worker
|
47
|
+
end
|
48
|
+
end
|
49
|
+
by_format
|
50
|
+
end
|
51
|
+
|
52
|
+
# Create list of workers sorted by run order
|
53
|
+
# Workers are initialized with options provided through options_proc
|
54
|
+
# Resolve all bins of all workers, if there are errors and
|
55
|
+
# skip_missing_workers of image_optim is true - show warnings, otherwise
|
56
|
+
# fail with one joint exception
|
57
|
+
def create_all(image_optim, &options_proc)
|
58
|
+
workers = init_all(image_optim, &options_proc)
|
59
|
+
|
60
|
+
resolved = []
|
61
|
+
errors = BinResolver.collect_errors(workers) do |worker|
|
62
|
+
worker.resolve_used_bins!
|
63
|
+
resolved << worker
|
64
|
+
end
|
65
|
+
|
66
|
+
unless errors.empty?
|
67
|
+
if image_optim.skip_missing_workers
|
68
|
+
errors.each{ |error| warn error }
|
69
|
+
else
|
70
|
+
message = ['Bin resolving errors:', *errors].join("\n")
|
71
|
+
fail BinResolver::Error, message
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
resolved.sort_by.with_index{ |worker, i| [worker.run_order, i] }
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def init_all(image_optim, &options_proc)
|
81
|
+
klasses.map do |klass|
|
82
|
+
next unless (options = options_proc[klass])
|
83
|
+
options = options.merge(:allow_lossy => image_optim.allow_lossy)
|
84
|
+
klass.init(image_optim, options)
|
85
|
+
end.compact.flatten
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -4,8 +4,25 @@ class ImageOptim
|
|
4
4
|
class Worker
|
5
5
|
# http://www.lcdf.org/gifsicle/
|
6
6
|
class Gifsicle < Worker
|
7
|
+
# If interlace specified initialize one instance
|
8
|
+
# Otherwise initialize two, one with interlace off and one with on
|
9
|
+
def self.init(image_optim, options = {})
|
10
|
+
return super if options.key?(:interlace)
|
11
|
+
|
12
|
+
[false, true].map do |interlace|
|
13
|
+
new(image_optim, options.merge(:interlace => interlace))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
7
17
|
INTERLACE_OPTION =
|
8
|
-
option(:interlace, false, '
|
18
|
+
option(:interlace, false, TrueFalseNil, 'Interlace: '\
|
19
|
+
'`true` - interlace on, '\
|
20
|
+
'`false` - interlace off, '\
|
21
|
+
'`nil` - as is in original image '\
|
22
|
+
'(defaults to running two instances, one with interlace off and '\
|
23
|
+
'one with on)') do |v|
|
24
|
+
TrueFalseNil.convert(v)
|
25
|
+
end
|
9
26
|
|
10
27
|
LEVEL_OPTION =
|
11
28
|
option(:level, 3, 'Compression level: '\
|
@@ -30,7 +47,13 @@ class ImageOptim
|
|
30
47
|
#{src}
|
31
48
|
]
|
32
49
|
|
33
|
-
|
50
|
+
if resolve_bin!(:gifsicle).version >= '1.85'
|
51
|
+
args.unshift('--no-extensions', '--no-app-extensions')
|
52
|
+
end
|
53
|
+
|
54
|
+
unless interlace.nil?
|
55
|
+
args.unshift(interlace ? '--interlace' : '--no-interlace')
|
56
|
+
end
|
34
57
|
args.unshift('--careful') if careful
|
35
58
|
args.unshift("--optimize=#{level}") if level
|
36
59
|
execute(:gifsicle, *args) && optimized?(src, dst)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'image_optim/worker'
|
2
|
+
require 'image_optim/option_helpers'
|
3
|
+
|
4
|
+
class ImageOptim
|
5
|
+
class Worker
|
6
|
+
# https://github.com/danielgtaylor/jpeg-archive#jpeg-recompress
|
7
|
+
class Jpegrecompress < Worker
|
8
|
+
# Initialize only if allow_lossy
|
9
|
+
def self.init(image_optim, options = {})
|
10
|
+
super if options[:allow_lossy]
|
11
|
+
end
|
12
|
+
|
13
|
+
QUALITY_NAMES = [:low, :medium, :high, :veryhigh]
|
14
|
+
|
15
|
+
quality_names_desc = QUALITY_NAMES.each_with_index.map do |name, i|
|
16
|
+
"`#{i}` - #{name}"
|
17
|
+
end.join(', ')
|
18
|
+
|
19
|
+
QUALITY_OPTION =
|
20
|
+
option(:quality, 3, "JPEG quality preset: #{quality_names_desc}") do |v|
|
21
|
+
OptionHelpers.limit_with_range(v.to_i, 0...QUALITY_NAMES.length)
|
22
|
+
end
|
23
|
+
|
24
|
+
def used_bins
|
25
|
+
[:'jpeg-recompress']
|
26
|
+
end
|
27
|
+
|
28
|
+
# Run first [-1]
|
29
|
+
def run_order
|
30
|
+
-5
|
31
|
+
end
|
32
|
+
|
33
|
+
def optimize(src, dst)
|
34
|
+
args = %W[
|
35
|
+
--quality #{QUALITY_NAMES[quality]}
|
36
|
+
--no-copy
|
37
|
+
#{src}
|
38
|
+
#{dst}
|
39
|
+
]
|
40
|
+
execute(:'jpeg-recompress', *args) && optimized?(src, dst)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -14,7 +14,7 @@ class ImageOptim
|
|
14
14
|
end
|
15
15
|
|
16
16
|
INTERLACE_OPTION =
|
17
|
-
option(:interlace, false, TrueFalseNil, 'Interlace
|
17
|
+
option(:interlace, false, TrueFalseNil, 'Interlace: '\
|
18
18
|
'`true` - interlace on, '\
|
19
19
|
'`false` - interlace off, '\
|
20
20
|
'`nil` - as is in original image') do |v|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# encoding: UTF-8
|
3
3
|
|
4
|
-
|
4
|
+
require 'bundler/setup'
|
5
5
|
|
6
6
|
require 'image_optim'
|
7
7
|
|
@@ -17,12 +17,11 @@ def write_worker_options(io, klass)
|
|
17
17
|
io.puts 'Worker has no options'
|
18
18
|
else
|
19
19
|
klass.option_definitions.each do |option_definition|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
].join(' ')
|
20
|
+
line = "* `:#{option_definition.name}` — #{option_definition.description}"
|
21
|
+
unless line['(defaults']
|
22
|
+
line << " *(defaults to `#{option_definition.default.inspect}`)*"
|
23
|
+
end
|
24
|
+
io.puts line
|
26
25
|
end
|
27
26
|
end
|
28
27
|
io.puts
|
data/script/worker_analysis
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# encoding: UTF-8
|
3
3
|
|
4
|
-
|
4
|
+
require 'bundler/setup'
|
5
5
|
|
6
6
|
require 'image_optim'
|
7
7
|
require 'image_optim/cmd'
|
@@ -141,12 +141,16 @@ class Analyser
|
|
141
141
|
|
142
142
|
# Delegate to worker with short id
|
143
143
|
class WorkerVariant < DelegateClass(ImageOptim::Worker)
|
144
|
-
attr_reader :
|
144
|
+
attr_reader :cons_id, :id
|
145
145
|
def initialize(klass, image_optim, options)
|
146
|
-
|
146
|
+
allow_consecutive_on = Array(options.delete(:allow_consecutive_on))
|
147
147
|
@image_optim = image_optim
|
148
|
-
@id =
|
148
|
+
@id = klass.bin_sym.to_s
|
149
|
+
unless options.empty?
|
150
|
+
@id << "(#{options.map{ |k, v| "#{k}:#{v.inspect}" }.join(', ')})"
|
151
|
+
end
|
149
152
|
__setobj__(klass.new(image_optim, options))
|
153
|
+
@cons_id = [klass, allow_consecutive_on.map{ |key| [key, send(key)] }]
|
150
154
|
end
|
151
155
|
|
152
156
|
def cache_etag
|
@@ -270,7 +274,7 @@ class Analyser
|
|
270
274
|
|
271
275
|
block.call(chain_result)
|
272
276
|
|
273
|
-
workers_left = workers.reject{ |w| w.
|
277
|
+
workers_left = workers.reject{ |w| w.cons_id == worker.cons_id }
|
274
278
|
run_workers(result_image, workers_left, chain_result, &block)
|
275
279
|
end
|
276
280
|
end
|
@@ -427,6 +431,7 @@ class Analyser
|
|
427
431
|
@workers_by_format = Hash.new{ |h, k| h[k] = [] }
|
428
432
|
ImageOptim::Worker.klasses.each do |klass|
|
429
433
|
worker_options_config = option_variants.delete(klass.bin_sym) || {}
|
434
|
+
allow_consecutive_on = worker_options_config.delete(:allow_consecutive_on)
|
430
435
|
worker_option_variants = case worker_options_config
|
431
436
|
when Array
|
432
437
|
worker_options_config
|
@@ -437,6 +442,7 @@ class Analyser
|
|
437
442
|
end
|
438
443
|
worker_option_variants.each do |options|
|
439
444
|
options = HashHelpers.deep_symbolise_keys(options)
|
445
|
+
options[:allow_consecutive_on] = allow_consecutive_on
|
440
446
|
worker = WorkerVariant.new(klass, image_optim, options)
|
441
447
|
puts worker.id
|
442
448
|
worker.image_formats.each do |format|
|
@@ -517,6 +523,10 @@ Example of `.analysis_variants.yml`:
|
|
517
523
|
optipng: # 6 worker variants by combining options
|
518
524
|
level: [6, 7]
|
519
525
|
interlace: [true, false, nil]
|
526
|
+
gifsicle: # allow variants with different interlace to run consecutively
|
527
|
+
allow_consecutive_on: interlace
|
528
|
+
interlace: [true, false]
|
529
|
+
careful: [true, false]
|
520
530
|
# other workers will be used with default options
|
521
531
|
HELP
|
522
532
|
end
|
data/script/worker_analysis.haml
CHANGED
@@ -55,14 +55,12 @@
|
|
55
55
|
table[data-sortable] tbody tr:hover {
|
56
56
|
background: #ddf;
|
57
57
|
}
|
58
|
-
tr.unused {
|
58
|
+
body:not(.show-unused) tr.unused, tr.filtered-out {
|
59
59
|
display: none;
|
60
60
|
}
|
61
61
|
|
62
|
-
input {
|
63
|
-
|
64
|
-
display: table-row;
|
65
|
-
}
|
62
|
+
input { display: block; width: 30%; }
|
63
|
+
input[type=checkbox] { display: inline; width: auto; }
|
66
64
|
|
67
65
|
.number {
|
68
66
|
text-align: right;
|
@@ -102,6 +100,24 @@
|
|
102
100
|
:javascript
|
103
101
|
/*! sortable.js 0.6.0 */
|
104
102
|
(function(){var a,b,c,d,e,f,g;a="table[data-sortable]",d=/^-?[£$¤]?[\d,.]+%?$/,g=/^\s+|\s+$/g,f="ontouchstart"in document.documentElement,c=f?"touchstart":"click",b=function(a,b,c){return null!=a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c)},e={init:function(b){var c,d,f,g,h;for(null==b&&(b={}),null==b.selector&&(b.selector=a),d=document.querySelectorAll(b.selector),h=[],f=0,g=d.length;g>f;f++)c=d[f],h.push(e.initTable(c));return h},initTable:function(a){var b,c,d,f,g,h;if(1===(null!=(h=a.tHead)?h.rows.length:void 0)&&"true"!==a.getAttribute("data-sortable-initialized")){for(a.setAttribute("data-sortable-initialized","true"),d=a.querySelectorAll("th"),b=f=0,g=d.length;g>f;b=++f)c=d[b],"false"!==c.getAttribute("data-sortable")&&e.setupClickableTH(a,c,b);return a}},setupClickableTH:function(a,d,f){var g;return g=e.getColumnType(a,f),"click"===c&&b(d,"mousedown",function(){return event.preventDefault?event.preventDefault():event.returnValue=!1}),b(d,c,function(){var b,c,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v;for("true"===this.getAttribute("data-sorted")?(l=this.getAttribute("data-sorted-direction"),b="ascending"===l?"descending":"ascending"):b=this.getAttribute("data-default-direction")||g.defaultSortDirection,n=this.parentNode.querySelectorAll("th"),o=0,r=n.length;r>o;o++)d=n[o],d.setAttribute("data-sorted","false"),d.removeAttribute("data-sorted-direction");for(this.setAttribute("data-sorted","true"),this.setAttribute("data-sorted-direction",b),m=a.tBodies[0],i=[],u=m.rows,c=p=0,s=u.length;s>p;c=++p)h=u[c],i.push([e.getNodeValue(h.cells[f]),h,c]);for(k="descending"===b?-1:1,i.sort(function(a,b){var c;return c=g.compare(a,b),0!==c?c*k:a[2]-b[2]}),v=[],q=0,t=i.length;t>q;q++)j=i[q],v.push(m.appendChild(j[1]));return v})},getColumnType:function(a,b){var c,f,g,h,i;for(i=a.tBodies[0].rows,g=0,h=i.length;h>g;g++)if(c=i[g],f=e.getNodeValue(c.cells[b]),""!==f){if(f.match(d))return e.types.numeric;if(!isNaN(Date.parse(f)))return e.types.date}return e.types.alpha},getNodeValue:function(a){return a?null!==a.getAttribute("data-value")?a.getAttribute("data-value"):"undefined"!=typeof a.innerText?a.innerText.replace(g,""):a.textContent.replace(g,""):""},types:{numeric:{defaultSortDirection:"descending",compare:function(a,b){var c,d;return c=parseFloat(a[0].replace(/[^0-9.-]/g,""),10),d=parseFloat(b[0].replace(/[^0-9.-]/g,""),10),isNaN(c)&&(c=0),isNaN(d)&&(d=0),c-d}},alpha:{defaultSortDirection:"ascending",compare:function(a,b){return a[0].localeCompare(b[0])}},date:{defaultSortDirection:"ascending",compare:function(a,b){var c,d;return c=Date.parse(a[0]),d=Date.parse(b[0]),isNaN(c)&&(c=0),isNaN(d)&&(d=0),c-d}}}},setTimeout(e.init,0),window.Sortable=e}).call(this);
|
103
|
+
:javascript
|
104
|
+
function toggleShowUnused(){
|
105
|
+
var checked = document.getElementById('toggle-show-unused').checked
|
106
|
+
document.body.classList[checked ? 'add' : 'remove']('show-unused')
|
107
|
+
}
|
108
|
+
function filterTable(){
|
109
|
+
var filter = new RegExp(document.getElementById('filter').value)
|
110
|
+
var rows = document.querySelectorAll('tbody.filterable tr')
|
111
|
+
for (var i = 0, _i = rows.length; i < _i; i++) {
|
112
|
+
var row = rows[i]
|
113
|
+
if (!row.hasAttribute('data-filter-text')) {
|
114
|
+
var text = row.textContent.replace(/\s+/g, ' ')
|
115
|
+
row.setAttribute('data-filter-text', text)
|
116
|
+
}
|
117
|
+
var show = filter.test(row.getAttribute('data-filter-text'))
|
118
|
+
row.classList[show ? 'remove' : 'add']('filtered-out')
|
119
|
+
}
|
120
|
+
}
|
105
121
|
%body
|
106
122
|
.formats
|
107
123
|
- format_links.each do |format, link|
|
@@ -114,9 +130,14 @@
|
|
114
130
|
%td.warn-low Max difference >= 0.001
|
115
131
|
%td.warn-medium Max difference >= 0.01
|
116
132
|
%td.warn-high Max difference >= 0.1
|
117
|
-
%
|
118
|
-
%label
|
133
|
+
%p
|
134
|
+
%label
|
135
|
+
%input#toggle-show-unused(type="checkbox" onchange="toggleShowUnused()")
|
119
136
|
Show chains with workers failed for every image
|
137
|
+
%p
|
138
|
+
%label(for="filter")
|
139
|
+
Filter chains by regexp
|
140
|
+
%input#filter(type="text" onkeyup="filterTable()")
|
120
141
|
%p
|
121
142
|
Images: #{stats.each_chain.first.entry_count},
|
122
143
|
size: #{stats.each_chain.first.original_size}
|
@@ -133,7 +154,7 @@
|
|
133
154
|
%th(data-default-direction="ascending") Avg difference
|
134
155
|
%th(data-default-direction="ascending") Max difference
|
135
156
|
%th Speed (B/s)
|
136
|
-
%tbody
|
157
|
+
%tbody.filterable
|
137
158
|
- stats.each_chain do |chain|
|
138
159
|
%tr{class: chain.unused_workers && 'unused'}
|
139
160
|
%td
|
@@ -144,8 +165,8 @@
|
|
144
165
|
.worker-success-count(title="successfull count")
|
145
166
|
= worker_stat.success_count
|
146
167
|
%td.number= chain.optimized_size
|
147
|
-
%td.number= format '%.
|
148
|
-
%td.number= format '%.
|
168
|
+
%td.number= format '%.5f', chain.ratio
|
169
|
+
%td.number= format '%.5f', chain.avg_ratio
|
149
170
|
%td.number= format '%.2f', chain.time
|
150
171
|
%td.number= format '%.5f', chain.avg_difference
|
151
172
|
%td.number{class: chain.warn_level && "warn-#{chain.warn_level}"}
|
@@ -102,7 +102,8 @@ describe ImageOptim::BinResolver do
|
|
102
102
|
expect(resolver).to receive(:full_path).with(:ls).and_return('/bin/ls')
|
103
103
|
bin = double
|
104
104
|
expect(Bin).to receive(:new).with(:ls, '/bin/ls').and_return(bin)
|
105
|
-
expect(bin).to receive(:check!).
|
105
|
+
expect(bin).to receive(:check!).once
|
106
|
+
expect(bin).to receive(:check_fail!).exactly(5).times
|
106
107
|
|
107
108
|
5.times do
|
108
109
|
expect(resolver.resolve!(:ls)).to eq(bin)
|
@@ -149,7 +150,8 @@ describe ImageOptim::BinResolver do
|
|
149
150
|
bin = double
|
150
151
|
expect(Bin).to receive(:new).
|
151
152
|
with(:image_optim, File.expand_path(path)).and_return(bin)
|
152
|
-
expect(bin).to receive(:check!).
|
153
|
+
expect(bin).to receive(:check!).once
|
154
|
+
expect(bin).to receive(:check_fail!).exactly(5).times
|
153
155
|
|
154
156
|
at_exit_blocks = []
|
155
157
|
expect(resolver).to receive(:at_exit).once do |&block|
|
@@ -202,9 +204,10 @@ describe ImageOptim::BinResolver do
|
|
202
204
|
bin = double
|
203
205
|
expect(Bin).to receive(:new).once.with(:ls, '/bin/ls').and_return(bin)
|
204
206
|
|
205
|
-
|
207
|
+
count = 0
|
206
208
|
mutex = Mutex.new
|
207
|
-
allow(bin).to receive(:check!)
|
209
|
+
allow(bin).to receive(:check!).once
|
210
|
+
allow(bin).to receive(:check_fail!){ mutex.synchronize{ count += 1 } }
|
208
211
|
|
209
212
|
10.times.map do
|
210
213
|
Thread.new do
|
@@ -212,29 +215,58 @@ describe ImageOptim::BinResolver do
|
|
212
215
|
end
|
213
216
|
end.each(&:join)
|
214
217
|
|
215
|
-
expect(
|
218
|
+
expect(count).to eq(10)
|
216
219
|
end
|
217
220
|
end
|
218
221
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
+
describe 'checking version' do
|
223
|
+
before do
|
224
|
+
allow(resolver).to receive(:full_path){ |name| "/bin/#{name}" }
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'raises every time if did not get bin version' do
|
228
|
+
with_env 'PNGCRUSH_BIN', nil do
|
229
|
+
bin = Bin.new(:pngcrush, '/bin/pngcrush')
|
230
|
+
|
231
|
+
expect(Bin).to receive(:new).and_return(bin)
|
232
|
+
allow(bin).to receive(:version).and_return(nil)
|
222
233
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
234
|
+
5.times do
|
235
|
+
expect do
|
236
|
+
resolver.resolve!(:pngcrush)
|
237
|
+
end.to raise_error Bin::UnknownVersion
|
238
|
+
end
|
239
|
+
end
|
227
240
|
end
|
228
|
-
end
|
229
241
|
|
230
|
-
|
231
|
-
|
232
|
-
|
242
|
+
it 'raises every time on detection of misbehaving version' do
|
243
|
+
with_env 'PNGCRUSH_BIN', nil do
|
244
|
+
bin = Bin.new(:pngcrush, '/bin/pngcrush')
|
245
|
+
|
246
|
+
expect(Bin).to receive(:new).and_return(bin)
|
247
|
+
allow(bin).to receive(:version).and_return(SimpleVersion.new('1.7.60'))
|
233
248
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
249
|
+
5.times do
|
250
|
+
expect do
|
251
|
+
resolver.resolve!(:pngcrush)
|
252
|
+
end.to raise_error Bin::BadVersion
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'warns once on detection of problematic version' do
|
258
|
+
with_env 'ADVPNG_BIN', nil do
|
259
|
+
bin = Bin.new(:advpng, '/bin/advpng')
|
260
|
+
|
261
|
+
expect(Bin).to receive(:new).and_return(bin)
|
262
|
+
allow(bin).to receive(:version).and_return(SimpleVersion.new('1.15'))
|
263
|
+
|
264
|
+
expect(bin).to receive(:warn).once
|
265
|
+
|
266
|
+
5.times do
|
267
|
+
resolver.resolve!(:pngcrush)
|
268
|
+
end
|
269
|
+
end
|
238
270
|
end
|
239
271
|
end
|
240
272
|
end
|