image_optim 0.6.0 → 0.7.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.
@@ -1,6 +1,7 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  require 'shellwords'
4
+
4
5
  require 'image_optim'
5
6
 
6
7
  class ImageOptim
@@ -16,12 +17,6 @@ class ImageOptim
16
17
  klasses << base
17
18
  end
18
19
 
19
- # List of formats which worker can optimize
20
- def image_formats
21
- format_from_name = name.downcase[/gif|jpeg|png/]
22
- format_from_name ? [format_from_name.to_sym] : []
23
- end
24
-
25
20
  # Undercored class name
26
21
  def underscored_name
27
22
  @underscored_name ||= name.split('::').last.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
@@ -30,59 +25,51 @@ class ImageOptim
30
25
 
31
26
  include OptionHelpers
32
27
 
33
- # Binary name or path
34
- attr_reader :bin
35
-
36
- # Binary name or path
37
- attr_reader :nice
38
-
39
- # Be verbose
40
- attr_reader :verbose
41
-
42
- # Configure (raises on extra options), find binary (raises if not found)
43
- def initialize(options = {})
44
- get_option!(options, :bin, default_bin)
45
- get_option!(options, :nice, 10){ |v| v.to_i }
46
- get_option!(options, :verbose, false)
28
+ # Configure (raises on extra options)
29
+ def initialize(image_optim, options = {})
30
+ @image_optim = image_optim
47
31
  parse_options(options)
48
- raise BinaryNotFoundError, "`#{bin}` not found" if `which #{bin.to_s.shellescape}`.empty?
49
32
  assert_options_empty!(options)
50
33
  end
51
34
 
52
- # Put first in list of workers
53
- def run_first?
35
+ # List of formats which worker can optimize
36
+ def image_formats
37
+ format_from_name = self.class.name.downcase[/gif|jpeg|png/]
38
+ format_from_name ? [format_from_name.to_sym] : []
54
39
  end
55
40
 
56
- # Priority in list of workers
57
- def run_priority
58
- run_first? ? 0 : 1
41
+ # Ordering in list of workers
42
+ def run_order
43
+ 0
59
44
  end
60
45
 
61
- # Optimize file from src to dst, return boolean representing success status
62
- def optimize(src, dst)
63
- command = [bin, *command_args(src, dst)].map(&:to_s).shelljoin
64
- start = Time.now
65
- pid = fork do
66
- $stdout.reopen('/dev/null', 'w')
67
- $stderr.reopen('/dev/null', 'w')
68
- Process.setpriority(Process::PRIO_PROCESS, 0, nice)
69
- exec command
70
- end
71
- Process.wait pid
72
- duration = Time.now - start
73
- if $?.signaled?
74
- raise SignalException.new($?.termsig)
75
- end
76
- success = $?.success? && dst.size? && dst.size < src.size
77
- if verbose
78
- print "#{success ? '✓' : '✗'} #{duration}s #{command}\n"
79
- end
80
- success
46
+ # Check if operation resulted in optimized file
47
+ def optimized?(src, dst)
48
+ dst.size? && dst.size < src.size
81
49
  end
82
50
 
83
- # Name of binary determined from class name
84
- def default_bin
85
- self.class.underscored_name
51
+ private
52
+
53
+ # Forward bin resolving to image_optim
54
+ def resolve_bin!(bin)
55
+ @image_optim.resolve_bin!(bin)
56
+ end
57
+
58
+ # Run command setting priority and hiding output
59
+ def execute(bin, *arguments)
60
+ resolve_bin!(bin)
61
+
62
+ command = [bin, *arguments].map(&:to_s).shelljoin
63
+ env_path = "#{@image_optim.resolve_dir}:#{ENV['PATH']}"
64
+ start = Time.now
65
+
66
+ system "env PATH=#{env_path.shellescape} nice -n #{@image_optim.nice} #{command} >& /dev/null"
67
+
68
+ raise SignalException.new($?.termsig) if $?.signaled?
69
+
70
+ $stderr << "#{$?.success? ? '✓' : '✗'} #{Time.now - start}s #{command}\n" if @image_optim.verbose?
71
+
72
+ $?.success?
86
73
  end
87
74
  end
88
75
  end
@@ -0,0 +1,22 @@
1
+ require 'image_optim/worker'
2
+
3
+ class ImageOptim
4
+ class Worker
5
+ class Advpng < Worker
6
+ # Compression level: 0 - don't compress, 1 - fast, 2 - normal, 3 - extra, 4 - extreme (defaults to 4)
7
+ attr_reader :level
8
+
9
+ def optimize(src, dst)
10
+ src.copy(dst)
11
+ args = %W[-#{level} -z -q -- #{dst}]
12
+ execute(:advpng, *args) && optimized?(src, dst)
13
+ end
14
+
15
+ private
16
+
17
+ def parse_options(options)
18
+ get_option!(options, :level, 4){ |v| limit_with_range(v.to_i, 0..4) }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'image_optim/worker'
2
+
3
+ class ImageOptim
4
+ class Worker
5
+ class Gifsicle < Worker
6
+ # Turn on interlacing (defaults to false)
7
+ attr_reader :interlace
8
+
9
+ def optimize(src, dst)
10
+ args = %W[-o #{dst} -O3 --no-comments --no-names --same-delay --same-loopcount --no-warnings -- #{src}]
11
+ args.unshift('-i') if interlace
12
+ execute(:gifsicle, *args) && optimized?(src, dst)
13
+ end
14
+
15
+ private
16
+
17
+ def parse_options(options)
18
+ get_option!(options, :interlace, false){ |v| !!v }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ require 'image_optim/worker'
2
+
3
+ class ImageOptim
4
+ class Worker
5
+ class Jpegoptim < Worker
6
+ # List of extra markers to strip: comments, exif, iptc, icc (defaults to 'all')
7
+ attr_reader :strip
8
+
9
+ # Maximum image quality factor (defaults to 100)
10
+ attr_reader :max_quality
11
+
12
+ # Run first if max_quality < 100
13
+ def run_order
14
+ max_quality < 100 ? -1 : 0
15
+ end
16
+
17
+ def optimize(src, dst)
18
+ src.copy(dst)
19
+ args = %W[-q -- #{dst}]
20
+ strip.each do |strip_marker|
21
+ args.unshift "--strip-#{strip_marker}"
22
+ end
23
+ args.unshift "-m#{max_quality}" if max_quality < 100
24
+ execute(:jpegoptim, *args) && optimized?(src, dst)
25
+ end
26
+
27
+ private
28
+
29
+ def parse_options(options)
30
+ get_option!(options, :strip, :all) do |v|
31
+ markers = Array(v).map(&:to_s)
32
+ possible_markers = %w[all comments exif iptc icc]
33
+ unknown_markers = markers - possible_markers
34
+ warn "Unknown markers for jpegoptim: #{unknown_markers.join(', ')}" unless unknown_markers.empty?
35
+ markers & possible_markers
36
+ end
37
+ get_option!(options, :max_quality, 100){ |v| v.to_i }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ require 'image_optim/worker'
2
+
3
+ class ImageOptim
4
+ class Worker
5
+ class Jpegtran < Worker
6
+ # Copy all chunks or none (defaults to false)
7
+ attr_reader :copy_chunks
8
+
9
+ # Create progressive JPEG file (defaults to true)
10
+ attr_reader :progressive
11
+
12
+ def optimize(src, dst)
13
+ args = %W[-optimize -outfile #{dst} #{src}]
14
+ args.unshift '-copy', copy_chunks ? 'all' : 'none'
15
+ args.unshift '-progressive' if progressive
16
+ execute(:jpegtran, *args) && optimized?(src, dst)
17
+ end
18
+
19
+ private
20
+
21
+ def parse_options(options)
22
+ get_option!(options, :copy_chunks, false){ |v| !!v }
23
+ get_option!(options, :progressive, true){ |v| !!v }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ require 'image_optim/worker'
2
+
3
+ class ImageOptim
4
+ class Worker
5
+ class Optipng < Worker
6
+ # Optimization level preset 0..7 (0 is least, 7 is best, defaults to 6)
7
+ attr_reader :level
8
+
9
+ # Interlace, true - interlace on, false - interlace off, nil - as is in original image (defaults to false)
10
+ attr_reader :interlace
11
+
12
+ def optimize(src, dst)
13
+ src.copy(dst)
14
+ args = %W[-o#{level} -quiet -- #{dst}]
15
+ args.unshift "-i#{interlace ? 1 : 0}" unless interlace.nil?
16
+ execute(:optipng, *args) && optimized?(src, dst)
17
+ end
18
+
19
+ private
20
+
21
+ def parse_options(options)
22
+ get_option!(options, :level, 6){ |v| limit_with_range(v.to_i, 0..7) }
23
+ get_option!(options, :interlace, false){ |v| v && true }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ require 'image_optim/worker'
2
+
3
+ class ImageOptim
4
+ class Worker
5
+ class Pngcrush < Worker
6
+ # List of chunks to remove or 'alla' - all except tRNS/transparency or 'allb' - all except tRNS and gAMA/gamma (defaults to 'alla')
7
+ attr_reader :chunks
8
+
9
+ # Fix otherwise fatal conditions such as bad CRCs (defaults to false)
10
+ attr_reader :fix
11
+
12
+ # Brute force try all methods, very time-consuming and generally not worthwhile (defaults to false)
13
+ attr_reader :brute
14
+
15
+ # Always run first
16
+ def run_order
17
+ -1
18
+ end
19
+
20
+ def optimize(src, dst)
21
+ args = %W[-reduce -cc -q -- #{src} #{dst}]
22
+ chunks.each do |chunk|
23
+ args.unshift '-rem', chunk
24
+ end
25
+ args.unshift '-fix' if fix
26
+ args.unshift '-brute' if brute
27
+ execute(:pngcrush, *args) && optimized?(src, dst)
28
+ end
29
+
30
+ private
31
+
32
+ def parse_options(options)
33
+ get_option!(options, :chunks, :alla){ |v| Array(v).map(&:to_s) }
34
+ get_option!(options, :fix, false){ |v| !!v }
35
+ get_option!(options, :brute, false){ |v| !!v }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ require 'image_optim/worker'
2
+
3
+ class ImageOptim
4
+ class Worker
5
+ class Pngout < Worker
6
+ # Copy optional chunks (defaults to false)
7
+ attr_reader :copy_chunks
8
+
9
+ # Strategy: 0 - xtreme, 1 - intense, 2 - longest Match, 3 - huffman Only, 4 - uncompressed (defaults to 0)
10
+ attr_reader :strategy
11
+
12
+ # Always run first
13
+ def run_order
14
+ -1
15
+ end
16
+
17
+ def optimize(src, dst)
18
+ args = %W[-k#{copy_chunks ? 1 : 0} -s#{strategy} -q -y #{src} #{dst}]
19
+ execute(:pngout, *args) && optimized?(src, dst)
20
+ end
21
+
22
+ private
23
+
24
+ def parse_options(options)
25
+ get_option!(options, :copy_chunks, false){ |v| !!v }
26
+ get_option!(options, :strategy, 0){ |v| limit_with_range(v.to_i, 0..4) }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,116 +1,153 @@
1
1
  $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
2
2
  require 'rspec'
3
3
  require 'image_optim'
4
+ require 'tempfile'
4
5
 
5
- spec_dir = ImageOptim::ImagePath.new(__FILE__).dirname.relative_path_from(Dir.pwd)
6
- image_dir = spec_dir / 'images'
6
+ TEST_IMAGES = (ImageOptim::ImagePath.new(__FILE__).dirname.relative_path_from(Dir.pwd) / 'images').glob('*')
7
7
 
8
- def temp_copy_path(original)
9
- original.class.temp_dir do |dir|
10
- temp_path = dir / original.basename
11
- original.copy(temp_path)
12
- yield temp_path
8
+ Fixnum.class_eval do
9
+ def in_range?(range)
10
+ range.include?(self)
13
11
  end
14
12
  end
15
13
 
16
- class Tempfile
17
- alias_method :initialize_orig, :initialize
18
-
19
- def initialize(*args, &block)
20
- self.class.initialize_called
21
- initialize_orig(*args, &block)
14
+ Tempfile.class_eval do
15
+ def self.init_count
16
+ class_variable_get(:@@init_count)
22
17
  end
23
18
 
24
- def self.initialize_called
25
- @@call_count ||= 0
26
- @@call_count += 1
19
+ def self.init_count=(value)
20
+ class_variable_set(:@@init_count, value)
27
21
  end
28
22
 
29
- def self.reset_call_count
30
- @@call_count = 0
23
+ def self.reset_init_count
24
+ self.init_count = 0
31
25
  end
32
26
 
33
- def self.call_count
34
- @@call_count
27
+ reset_init_count
28
+
29
+ alias_method :initialize_orig, :initialize
30
+ def initialize(*args, &block)
31
+ self.class.init_count += 1
32
+ initialize_orig(*args, &block)
35
33
  end
36
34
  end
37
35
 
38
- Fixnum.class_eval do
39
- def in_range?(range)
40
- range.include?(self)
36
+ ImageOptim::ImagePath.class_eval do
37
+ def temp_copy
38
+ temp_path.tap{ |path| copy(path) }
41
39
  end
42
40
  end
43
41
 
42
+ def with_env(key, value)
43
+ saved, ENV[key] = ENV[key], value
44
+ yield
45
+ ensure
46
+ ENV[key] = saved
47
+ end
48
+
44
49
  describe ImageOptim do
45
- image_dir.glob('*') do |original|
46
- describe "optimizing #{original}" do
47
- it "should optimize image" do
48
- temp_copy_path(original) do |unoptimized|
49
- Tempfile.reset_call_count
50
- io = ImageOptim.new
51
- optimized = io.optimize_image(unoptimized)
52
- optimized.should be_a(FSPath)
53
- unoptimized.read.should == original.read
54
- optimized.size.should > 0
55
- optimized.size.should < unoptimized.size
56
- optimized.read.should_not == unoptimized.read
57
- if io.workers_for_image(unoptimized).length > 1
58
- Tempfile.call_count.should be_in_range(1..2)
50
+ describe "isolated" do
51
+ describe "optimize" do
52
+ TEST_IMAGES.each do |original|
53
+ it "should optimize #{original}" do
54
+ copy = original.temp_copy
55
+
56
+ Tempfile.reset_init_count
57
+ image_optim = ImageOptim.new
58
+ optimized_image = image_optim.optimize_image(copy)
59
+ optimized_image.should be_a(ImageOptim::ImagePath)
60
+ optimized_image.size.should be_in_range(1...original.size)
61
+ optimized_image.read.should_not == original.read
62
+ copy.read.should == original.read
63
+
64
+ if image_optim.workers_for_image(original).length > 1
65
+ Tempfile.init_count.should be_in_range(1..2)
59
66
  else
60
- Tempfile.call_count.should === 1
67
+ Tempfile.init_count.should === 1
61
68
  end
62
69
  end
63
70
  end
71
+ end
72
+
73
+ describe "optimize in place" do
74
+ TEST_IMAGES.each do |original|
75
+ it "should optimize #{original}" do
76
+ copy = original.temp_copy
64
77
 
65
- it "should optimize image in place" do
66
- temp_copy_path(original) do |path|
67
- Tempfile.reset_call_count
68
- io = ImageOptim.new
69
- io.optimize_image!(path).should be_true
70
- path.size.should > 0
71
- path.size.should < original.size
72
- path.read.should_not == original.read
73
- if io.workers_for_image(path).length > 1
74
- Tempfile.call_count.should be_in_range(2..3)
78
+ Tempfile.reset_init_count
79
+ image_optim = ImageOptim.new
80
+ image_optim.optimize_image!(copy).should be_true
81
+ copy.size.should be_in_range(1...original.size)
82
+ copy.read.should_not == original.read
83
+
84
+ if image_optim.workers_for_image(original).length > 1
85
+ Tempfile.init_count.should be_in_range(2..3)
75
86
  else
76
- Tempfile.call_count.should === 2
87
+ Tempfile.init_count.should === 2
77
88
  end
78
89
  end
79
90
  end
91
+ end
80
92
 
81
- it "should stop optimizing" do
82
- temp_copy_path(original) do |unoptimized|
83
- count = (1..10).find do |i|
84
- unoptimized = ImageOptim.optimize_image(unoptimized)
85
- unoptimized.nil?
93
+ describe "stop optimizing" do
94
+ TEST_IMAGES.each do |original|
95
+ it "should stop optimizing #{original}" do
96
+ copy = original.temp_copy
97
+
98
+ tries = 0
99
+ 10.times do
100
+ tries += 1
101
+ break unless ImageOptim.optimize_image!(copy)
86
102
  end
87
- count.should >= 2
88
- count.should < 10
103
+ tries.should be_in_range(2...3)
89
104
  end
90
105
  end
91
106
  end
92
107
  end
93
108
 
94
- describe "unsupported file" do
109
+ describe "bunch" do
110
+ it "should optimize" do
111
+ copies = TEST_IMAGES.map(&:temp_copy)
112
+ optimized_images = ImageOptim.optimize_images(copies)
113
+ TEST_IMAGES.zip(copies, optimized_images).each do |original, copy, optimized_image|
114
+ optimized_image.should be_a(ImageOptim::ImagePath)
115
+ optimized_image.size.should be_in_range(1...original.size)
116
+ optimized_image.read.should_not == original.read
117
+ copy.read.should == original.read
118
+ end
119
+ end
120
+
121
+ it "should optimize in place" do
122
+ copies = TEST_IMAGES.map(&:temp_copy)
123
+ ImageOptim.optimize_images!(copies)
124
+ TEST_IMAGES.zip(copies).each do |original, copy|
125
+ copy.size.should be_in_range(1...original.size)
126
+ copy.read.should_not == original.read
127
+ end
128
+ end
129
+ end
130
+
131
+ describe "unsupported" do
95
132
  let(:original){ ImageOptim::ImagePath.new(__FILE__) }
96
133
 
97
134
  it "should ignore" do
98
- temp_copy_path(original) do |unoptimized|
99
- Tempfile.reset_call_count
100
- optimized = ImageOptim.optimize_image(unoptimized)
101
- Tempfile.call_count.should == 0
102
- optimized.should be_nil
103
- unoptimized.read.should == original.read
104
- end
135
+ copy = original.temp_copy
136
+
137
+ Tempfile.reset_init_count
138
+ optimized_image = ImageOptim.optimize_image(copy)
139
+ Tempfile.init_count.should == 0
140
+ optimized_image.should be_nil
141
+ copy.read.should == original.read
105
142
  end
106
143
 
107
144
  it "should ignore in place" do
108
- temp_copy_path(original) do |unoptimized|
109
- Tempfile.reset_call_count
110
- ImageOptim.optimize_image!(unoptimized).should_not be_true
111
- Tempfile.call_count.should == 0
112
- unoptimized.read.should == original.read
113
- end
145
+ copy = original.temp_copy
146
+
147
+ Tempfile.reset_init_count
148
+ ImageOptim.optimize_image!(copy).should_not be_true
149
+ Tempfile.init_count.should == 0
150
+ copy.read.should == original.read
114
151
  end
115
152
  end
116
153
 
@@ -124,34 +161,118 @@ describe ImageOptim do
124
161
  end
125
162
 
126
163
  %w[optimize_images optimize_images!].each do |list_method|
127
- single_method = list_method.sub('images', 'image')
128
- describe "without block" do
129
- it "should optimize images and return array of results" do
130
- io = ImageOptim.new
131
- dsts = []
132
- srcs.each do |src|
133
- dst = "#{src}_"
134
- io.should_receive(single_method).with(src).and_return(dst)
135
- dsts << dst
164
+ describe list_method do
165
+ single_method = list_method.sub('images', 'image')
166
+ describe "without block" do
167
+ it "should optimize images and return array of results" do
168
+ image_optim = ImageOptim.new
169
+ dsts = srcs.map do |src|
170
+ dst = "#{src}_"
171
+ image_optim.should_receive(single_method).with(src).and_return(dst)
172
+ dst
173
+ end
174
+ image_optim.send(list_method, srcs).should == dsts
136
175
  end
137
- io.send(list_method, srcs).should == dsts
138
176
  end
139
- end
140
177
 
141
- describe "given block" do
142
- it "should optimize images, yield path and result for each and return array of yield results" do
143
- io = ImageOptim.new
144
- results = []
145
- srcs.each do |src|
146
- dst = "#{src}_"
147
- io.should_receive(single_method).with(src).and_return(dst)
148
- results << "#{src} #{dst}"
178
+ describe "given block" do
179
+ it "should optimize images, yield path and result for each and return array of yield results" do
180
+ image_optim = ImageOptim.new
181
+ results = srcs.map do |src|
182
+ dst = "#{src}_"
183
+ image_optim.should_receive(single_method).with(src).and_return(dst)
184
+ "#{src} #{dst}"
185
+ end
186
+ image_optim.send(list_method, srcs) do |src, dst|
187
+ "#{src} #{dst}"
188
+ end.should == results
149
189
  end
150
- io.send(list_method, srcs) do |src, dst|
151
- "#{src} #{dst}"
152
- end.should == results
153
190
  end
154
191
  end
155
192
  end
156
193
  end
194
+
195
+ describe "resolve bin" do
196
+ it "should resolve bin in path" do
197
+ with_env 'LS_BIN', nil do
198
+ image_optim = ImageOptim.new
199
+ image_optim.should_receive(:bin_accessible?).with(:ls).once.and_return(true)
200
+ FSPath.should_not_receive(:temp_dir)
201
+
202
+ 5.times do
203
+ image_optim.resolve_bin!(:ls).should be_true
204
+ end
205
+ end
206
+ end
207
+
208
+ it "should resolve bin specified in ENV" do
209
+ path = (FSPath(__FILE__).dirname / '../bin/image_optim').relative_path_from(Dir.pwd).to_s
210
+ with_env 'IMAGE_OPTIM_BIN', path do
211
+ tmpdir = stub(:tmpdir)
212
+ symlink = stub(:symlink)
213
+
214
+ image_optim = ImageOptim.new
215
+ image_optim.should_receive(:bin_accessible?).with(symlink).once.and_return(true)
216
+ FSPath.should_receive(:temp_dir).once.and_return(tmpdir)
217
+ tmpdir.should_receive(:/).with(:image_optim).once.and_return(symlink)
218
+ symlink.should_receive(:make_symlink).with(File.expand_path(path)).once
219
+
220
+ at_exit_blocks = []
221
+ image_optim.should_receive(:at_exit).twice do |&block|
222
+ at_exit_blocks.unshift(block)
223
+ end
224
+
225
+ 5.times do
226
+ image_optim.resolve_bin!(:image_optim).should be_true
227
+ end
228
+
229
+ FileUtils.should_receive(:remove_entry_secure).with(tmpdir)
230
+ symlink.should_receive(:unlink)
231
+ at_exit_blocks.each(&:call)
232
+ end
233
+ end
234
+
235
+ it "should raise on failure to resolve bin" do
236
+ with_env 'SHOULD_NOT_EXIST_BIN', nil do
237
+ image_optim = ImageOptim.new
238
+ image_optim.should_receive(:bin_accessible?).with(:should_not_exist).once.and_return(false)
239
+ FSPath.should_not_receive(:temp_dir)
240
+
241
+ 5.times do
242
+ expect do
243
+ image_optim.resolve_bin!(:should_not_exist)
244
+ end.to raise_error ImageOptim::BinNotFoundError
245
+ end
246
+ end
247
+ end
248
+
249
+ it "should raise on failure to resolve bin specified in ENV" do
250
+ path = (FSPath(__FILE__).dirname / '../bin/should_not_exist_bin').relative_path_from(Dir.pwd).to_s
251
+ with_env 'SHOULD_NOT_EXIST_BIN', path do
252
+ tmpdir = stub(:tmpdir)
253
+ symlink = stub(:symlink)
254
+
255
+ image_optim = ImageOptim.new
256
+ image_optim.should_receive(:bin_accessible?).with(symlink).once.and_return(false)
257
+ FSPath.should_receive(:temp_dir).once.and_return(tmpdir)
258
+ tmpdir.should_receive(:/).with(:should_not_exist).once.and_return(symlink)
259
+ symlink.should_receive(:make_symlink).with(File.expand_path(path)).once
260
+
261
+ at_exit_blocks = []
262
+ image_optim.should_receive(:at_exit).twice do |&block|
263
+ at_exit_blocks.unshift(block)
264
+ end
265
+
266
+ 5.times do
267
+ expect do
268
+ image_optim.resolve_bin!(:should_not_exist)
269
+ end.to raise_error ImageOptim::BinNotFoundError
270
+ end
271
+
272
+ FileUtils.should_receive(:remove_entry_secure).with(tmpdir)
273
+ symlink.should_receive(:unlink)
274
+ at_exit_blocks.each(&:call)
275
+ end
276
+ end
277
+ end
157
278
  end