image_optim 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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