image_optim 0.26.5 → 0.30.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.
- checksums.yaml +4 -4
- data/.appveyor.yml +2 -0
- data/.pre-commit-hooks.yaml +9 -0
- data/.rubocop.yml +33 -14
- data/.travis.yml +17 -15
- data/CHANGELOG.markdown +26 -0
- data/CONTRIBUTING.markdown +4 -1
- data/LICENSE.txt +1 -1
- data/README.markdown +9 -3
- data/Vagrantfile +1 -1
- data/image_optim.gemspec +7 -4
- data/lib/image_optim.rb +15 -9
- data/lib/image_optim/bin_resolver/bin.rb +10 -8
- data/lib/image_optim/bin_resolver/comparable_condition.rb +1 -0
- data/lib/image_optim/cache.rb +6 -0
- data/lib/image_optim/cache_path.rb +19 -2
- data/lib/image_optim/cmd.rb +45 -6
- data/lib/image_optim/config.rb +13 -7
- data/lib/image_optim/elapsed_time.rb +26 -0
- data/lib/image_optim/errors.rb +9 -0
- data/lib/image_optim/optimized_path.rb +1 -1
- data/lib/image_optim/path.rb +29 -6
- data/lib/image_optim/runner/option_parser.rb +6 -0
- data/lib/image_optim/timer.rb +25 -0
- data/lib/image_optim/worker.rb +29 -26
- data/lib/image_optim/worker/advpng.rb +2 -2
- data/lib/image_optim/worker/class_methods.rb +2 -0
- data/lib/image_optim/worker/gifsicle.rb +3 -3
- data/lib/image_optim/worker/jhead.rb +2 -2
- data/lib/image_optim/worker/jpegoptim.rb +8 -6
- data/lib/image_optim/worker/jpegrecompress.rb +17 -2
- data/lib/image_optim/worker/jpegtran.rb +3 -3
- data/lib/image_optim/worker/optipng.rb +4 -4
- data/lib/image_optim/worker/pngcrush.rb +4 -4
- data/lib/image_optim/worker/pngout.rb +2 -2
- data/lib/image_optim/worker/pngquant.rb +3 -2
- data/lib/image_optim/worker/svgo.rb +2 -2
- data/script/update_worker_options_in_readme +1 -1
- data/script/worker_analysis +20 -19
- data/spec/image_optim/bin_resolver_spec.rb +5 -5
- data/spec/image_optim/cache_path_spec.rb +67 -28
- data/spec/image_optim/cache_spec.rb +10 -8
- data/spec/image_optim/cmd_spec.rb +58 -6
- data/spec/image_optim/config_spec.rb +36 -20
- data/spec/image_optim/elapsed_time_spec.rb +14 -0
- 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 +61 -26
- 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/pngquant_spec.rb +5 -5
- data/spec/image_optim/worker_spec.rb +17 -17
- data/spec/image_optim_spec.rb +50 -12
- data/spec/images/quant/generate +2 -2
- data/spec/spec_helper.rb +16 -15
- metadata +36 -12
|
@@ -12,7 +12,7 @@ class ImageOptim
|
|
|
12
12
|
return super if options.key?(:interlace)
|
|
13
13
|
|
|
14
14
|
[false, true].map do |interlace|
|
|
15
|
-
new(image_optim, options.merge(:
|
|
15
|
+
new(image_optim, options.merge(interlace: interlace))
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -37,7 +37,7 @@ class ImageOptim
|
|
|
37
37
|
CAREFUL_OPTION =
|
|
38
38
|
option(:careful, false, 'Avoid bugs with some software'){ |v| !!v }
|
|
39
39
|
|
|
40
|
-
def optimize(src, dst)
|
|
40
|
+
def optimize(src, dst, options = {})
|
|
41
41
|
args = %W[
|
|
42
42
|
--output=#{dst}
|
|
43
43
|
--no-comments
|
|
@@ -58,7 +58,7 @@ class ImageOptim
|
|
|
58
58
|
end
|
|
59
59
|
args.unshift '--careful' if careful
|
|
60
60
|
args.unshift "--optimize=#{level}" if level
|
|
61
|
-
execute(:gifsicle,
|
|
61
|
+
execute(:gifsicle, args, options) && optimized?(src, dst)
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
end
|
|
@@ -25,7 +25,7 @@ class ImageOptim
|
|
|
25
25
|
[:jhead, :jpegtran]
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def optimize(src, dst)
|
|
28
|
+
def optimize(src, dst, options = {})
|
|
29
29
|
return false unless oriented?(src)
|
|
30
30
|
|
|
31
31
|
src.copy(dst)
|
|
@@ -34,7 +34,7 @@ class ImageOptim
|
|
|
34
34
|
#{dst}
|
|
35
35
|
]
|
|
36
36
|
resolve_bin!(:jpegtran)
|
|
37
|
-
execute(:jhead,
|
|
37
|
+
execute(:jhead, args, options) && dst.size?
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
private
|
|
@@ -11,14 +11,16 @@ class ImageOptim
|
|
|
11
11
|
option(:allow_lossy, false, 'Allow limiting maximum quality'){ |v| !!v }
|
|
12
12
|
|
|
13
13
|
STRIP_OPTION =
|
|
14
|
-
option(:strip, :all, Array, 'List of
|
|
15
|
-
'`:
|
|
14
|
+
option(:strip, :all, Array, 'List of markers to strip: '\
|
|
15
|
+
'`:com`, '\
|
|
16
16
|
'`:exif`, '\
|
|
17
17
|
'`:iptc`, '\
|
|
18
|
-
'`:icc
|
|
18
|
+
'`:icc`, '\
|
|
19
|
+
'`:xmp`, '\
|
|
20
|
+
'`:none` or '\
|
|
19
21
|
'`:all`') do |v|
|
|
20
22
|
values = Array(v).map(&:to_s)
|
|
21
|
-
known_values = %w[
|
|
23
|
+
known_values = %w[com exif iptc icc xmp none all]
|
|
22
24
|
unknown_values = values - known_values
|
|
23
25
|
unless unknown_values.empty?
|
|
24
26
|
warn "Unknown markers for jpegoptim: #{unknown_values.join(', ')}"
|
|
@@ -45,7 +47,7 @@ class ImageOptim
|
|
|
45
47
|
max_quality < 100 ? -1 : 0
|
|
46
48
|
end
|
|
47
49
|
|
|
48
|
-
def optimize(src, dst)
|
|
50
|
+
def optimize(src, dst, options = {})
|
|
49
51
|
src.copy(dst)
|
|
50
52
|
args = %W[
|
|
51
53
|
--quiet
|
|
@@ -56,7 +58,7 @@ class ImageOptim
|
|
|
56
58
|
args.unshift "--strip-#{strip_marker}"
|
|
57
59
|
end
|
|
58
60
|
args.unshift "--max=#{max_quality}" if max_quality < 100
|
|
59
|
-
execute(:jpegoptim,
|
|
61
|
+
execute(:jpegoptim, args, options) && optimized?(src, dst)
|
|
60
62
|
end
|
|
61
63
|
end
|
|
62
64
|
end
|
|
@@ -26,6 +26,20 @@ class ImageOptim
|
|
|
26
26
|
OptionHelpers.limit_with_range(v.to_i, 0...QUALITY_NAMES.length)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
METHOD_OPTION =
|
|
30
|
+
option(:method, 'ssim', 'Comparison Metric: '\
|
|
31
|
+
'`mpe` - Mean pixel error, '\
|
|
32
|
+
'`ssim` - Structural similarity, '\
|
|
33
|
+
'`ms-ssim` - Multi-scale structural similarity (slow!), '\
|
|
34
|
+
'`smallfry` - Linear-weighted BBCQ-like (may be patented)') do |v, opt_def|
|
|
35
|
+
if %w[mpe ssim ms-ssim smallfry].include? v
|
|
36
|
+
v
|
|
37
|
+
else
|
|
38
|
+
warn "Unknown method for jpegrecompress: #{v}"
|
|
39
|
+
opt_def.default
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
29
43
|
def used_bins
|
|
30
44
|
[:'jpeg-recompress']
|
|
31
45
|
end
|
|
@@ -35,14 +49,15 @@ class ImageOptim
|
|
|
35
49
|
-5
|
|
36
50
|
end
|
|
37
51
|
|
|
38
|
-
def optimize(src, dst)
|
|
52
|
+
def optimize(src, dst, options = {})
|
|
39
53
|
args = %W[
|
|
40
54
|
--quality #{QUALITY_NAMES[quality]}
|
|
55
|
+
--method #{method}
|
|
41
56
|
--no-copy
|
|
42
57
|
#{src}
|
|
43
58
|
#{dst}
|
|
44
59
|
]
|
|
45
|
-
execute(:'jpeg-recompress',
|
|
60
|
+
execute(:'jpeg-recompress', args, options) && optimized?(src, dst)
|
|
46
61
|
end
|
|
47
62
|
end
|
|
48
63
|
end
|
|
@@ -23,7 +23,7 @@ class ImageOptim
|
|
|
23
23
|
jpegrescan ? [:jpegtran, :jpegrescan] : [:jpegtran]
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def optimize(src, dst)
|
|
26
|
+
def optimize(src, dst, options = {})
|
|
27
27
|
if jpegrescan
|
|
28
28
|
args = %W[
|
|
29
29
|
#{src}
|
|
@@ -31,7 +31,7 @@ class ImageOptim
|
|
|
31
31
|
]
|
|
32
32
|
args.unshift '-s' unless copy_chunks
|
|
33
33
|
resolve_bin!(:jpegtran)
|
|
34
|
-
execute(:jpegrescan,
|
|
34
|
+
execute(:jpegrescan, args, options) && optimized?(src, dst)
|
|
35
35
|
else
|
|
36
36
|
args = %W[
|
|
37
37
|
-optimize
|
|
@@ -40,7 +40,7 @@ class ImageOptim
|
|
|
40
40
|
]
|
|
41
41
|
args.unshift '-copy', (copy_chunks ? 'all' : 'none')
|
|
42
42
|
args.unshift '-progressive' if progressive
|
|
43
|
-
execute(:jpegtran,
|
|
43
|
+
execute(:jpegtran, args, options) && optimized?(src, dst)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
end
|
|
@@ -30,7 +30,7 @@ class ImageOptim
|
|
|
30
30
|
-4
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def optimize(src, dst)
|
|
33
|
+
def optimize(src, dst, options = {})
|
|
34
34
|
src.copy(dst)
|
|
35
35
|
args = %W[
|
|
36
36
|
-o #{level}
|
|
@@ -39,10 +39,10 @@ class ImageOptim
|
|
|
39
39
|
#{dst}
|
|
40
40
|
]
|
|
41
41
|
args.unshift "-i#{interlace ? 1 : 0}" unless interlace.nil?
|
|
42
|
-
if resolve_bin!(:optipng).version >= '0.7'
|
|
43
|
-
args.unshift '-strip', 'all'
|
|
42
|
+
if strip && resolve_bin!(:optipng).version >= '0.7'
|
|
43
|
+
args.unshift '-strip', 'all'
|
|
44
44
|
end
|
|
45
|
-
execute(:optipng,
|
|
45
|
+
execute(:optipng, args, options) && optimized?(src, dst)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def optimized?(src, dst)
|
|
@@ -28,7 +28,7 @@ class ImageOptim
|
|
|
28
28
|
-6
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def optimize(src, dst)
|
|
31
|
+
def optimize(src, dst, options = {})
|
|
32
32
|
flags = %w[
|
|
33
33
|
-reduce
|
|
34
34
|
-cc
|
|
@@ -39,8 +39,8 @@ class ImageOptim
|
|
|
39
39
|
end
|
|
40
40
|
flags.push '-fix' if fix
|
|
41
41
|
flags.push '-brute' if brute
|
|
42
|
-
if resolve_bin!(:pngcrush).version >= '1.7.38'
|
|
43
|
-
flags.push '-blacken'
|
|
42
|
+
if blacken && resolve_bin!(:pngcrush).version >= '1.7.38'
|
|
43
|
+
flags.push '-blacken'
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
args = flags + %W[
|
|
@@ -49,7 +49,7 @@ class ImageOptim
|
|
|
49
49
|
#{dst}
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
-
execute(:pngcrush,
|
|
52
|
+
execute(:pngcrush, args, options) && optimized?(src, dst)
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
end
|
|
@@ -24,7 +24,7 @@ class ImageOptim
|
|
|
24
24
|
2
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def optimize(src, dst)
|
|
27
|
+
def optimize(src, dst, options = {})
|
|
28
28
|
args = %W[
|
|
29
29
|
-k#{copy_chunks ? 1 : 0}
|
|
30
30
|
-s#{strategy}
|
|
@@ -33,7 +33,7 @@ class ImageOptim
|
|
|
33
33
|
#{src}
|
|
34
34
|
#{dst}
|
|
35
35
|
]
|
|
36
|
-
execute(:pngout,
|
|
36
|
+
execute(:pngout, args, options) && optimized?(src, dst)
|
|
37
37
|
rescue SignalException => e
|
|
38
38
|
raise unless Signal.list.key(e.signo) == 'SEGV'
|
|
39
39
|
raise unless resolve_bin!(:pngout).version <= '20150920'
|
|
@@ -50,17 +50,18 @@ class ImageOptim
|
|
|
50
50
|
-2
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
def optimize(src, dst)
|
|
53
|
+
def optimize(src, dst, options = {})
|
|
54
54
|
args = %W[
|
|
55
55
|
--quality=#{quality.begin}-#{quality.end}
|
|
56
56
|
--speed=#{speed}
|
|
57
57
|
--output=#{dst}
|
|
58
|
+
--skip-if-larger
|
|
58
59
|
--force
|
|
59
60
|
#{max_colors}
|
|
60
61
|
--
|
|
61
62
|
#{src}
|
|
62
63
|
]
|
|
63
|
-
execute(:pngquant,
|
|
64
|
+
execute(:pngquant, args, options) && optimized?(src, dst)
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
67
|
end
|
|
@@ -16,7 +16,7 @@ class ImageOptim
|
|
|
16
16
|
Array(v).map(&:to_s)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def optimize(src, dst)
|
|
19
|
+
def optimize(src, dst, options = {})
|
|
20
20
|
args = %W[
|
|
21
21
|
--input #{src}
|
|
22
22
|
--output #{dst}
|
|
@@ -27,7 +27,7 @@ class ImageOptim
|
|
|
27
27
|
enable_plugins.each do |plugin_name|
|
|
28
28
|
args.unshift "--enable=#{plugin_name}"
|
|
29
29
|
end
|
|
30
|
-
execute(:svgo,
|
|
30
|
+
execute(:svgo, args, options) && optimized?(src, dst)
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -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
|
data/script/worker_analysis
CHANGED
|
@@ -137,13 +137,14 @@ class Analyser
|
|
|
137
137
|
# Delegate to worker with short id
|
|
138
138
|
class WorkerVariant < DelegateClass(ImageOptim::Worker)
|
|
139
139
|
attr_reader :name, :id, :cons_id, :required
|
|
140
|
+
|
|
140
141
|
def initialize(klass, image_optim, options)
|
|
141
142
|
@required = options.delete(:required)
|
|
142
143
|
@run_order = options.delete(:run_order)
|
|
143
144
|
allow_consecutive_on = Array(options.delete(:allow_consecutive_on))
|
|
144
145
|
@image_optim = image_optim
|
|
145
146
|
@name = klass.bin_sym.to_s + options_string(options)
|
|
146
|
-
|
|
147
|
+
super(klass.new(image_optim, options))
|
|
147
148
|
@id = klass.bin_sym.to_s + options_string(self.options)
|
|
148
149
|
@cons_id = [klass, allow_consecutive_on.map{ |key| [key, send(key)] }]
|
|
149
150
|
end
|
|
@@ -357,20 +358,18 @@ class Analyser
|
|
|
357
358
|
end
|
|
358
359
|
|
|
359
360
|
def flatten_animation(image)
|
|
360
|
-
run_cache[:flatten][image.digest] ||=
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
image
|
|
373
|
-
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
|
|
374
373
|
end
|
|
375
374
|
end
|
|
376
375
|
|
|
@@ -452,6 +451,7 @@ class Analyser
|
|
|
452
451
|
attr_reader :name
|
|
453
452
|
attr_reader :success_count
|
|
454
453
|
attr_reader :time, :avg_time
|
|
454
|
+
|
|
455
455
|
def initialize(name, steps)
|
|
456
456
|
@name = name
|
|
457
457
|
@success_count = steps.count(&:success)
|
|
@@ -465,6 +465,7 @@ class Analyser
|
|
|
465
465
|
end
|
|
466
466
|
|
|
467
467
|
attr_reader :name, :results, :ids2names
|
|
468
|
+
|
|
468
469
|
def initialize(name, results, ids2names)
|
|
469
470
|
@name = name.to_s
|
|
470
471
|
@results = results
|
|
@@ -524,10 +525,10 @@ class Analyser
|
|
|
524
525
|
stats = Stats.new('all', by_format[format], worker_ids2names)
|
|
525
526
|
path = FSPath("#{DIR}/#{basenames[format]}")
|
|
526
527
|
model = {
|
|
527
|
-
:
|
|
528
|
-
:
|
|
529
|
-
:
|
|
530
|
-
:
|
|
528
|
+
stats_format: format,
|
|
529
|
+
stats: stats,
|
|
530
|
+
format_links: basenames,
|
|
531
|
+
template_dir: template_path.dirname.relative_path_from(path.dirname),
|
|
531
532
|
}
|
|
532
533
|
html = template.result(OpenStruct.new(model).instance_eval{ binding })
|
|
533
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,55 +5,94 @@ 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
|
|
9
|
+
stub_const('Path', ImageOptim::Path)
|
|
11
10
|
stub_const('CachePath', ImageOptim::CachePath)
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
describe '#replace' do
|
|
15
|
-
let(:
|
|
16
|
-
let(:
|
|
14
|
+
let(:src_dir){ Path.temp_dir }
|
|
15
|
+
let(:src){ CachePath.temp_file_path(nil, src_dir) }
|
|
16
|
+
let(:dst){ Path.temp_file_path }
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
shared_examples 'replaces file' do
|
|
19
|
+
it 'moves data to destination' do
|
|
20
|
+
src.write('src')
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
src.replace(dst)
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
expect(dst.read).to eq('src')
|
|
25
|
+
end
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
it 'does not remove original file' do
|
|
28
|
+
src.replace(dst)
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
expect(src).to exist
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'preserves attributes of destination file', skip: SkipConditions[:any_file_mode_allowed] do
|
|
34
|
+
mode = 0o666
|
|
35
|
+
|
|
36
|
+
dst.chmod(mode)
|
|
37
|
+
|
|
38
|
+
src.replace(dst)
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
got = dst.stat.mode & 0o777
|
|
41
|
+
expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
|
|
42
|
+
end
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
it 'does not preserve mtime of destination file' do
|
|
45
|
+
time = src.mtime
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
dst.utime(time - 1000, time - 1000)
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
src.replace(dst)
|
|
50
|
+
|
|
51
|
+
expect(dst.mtime).to be >= time
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'changes inode of destination', skip: SkipConditions[:inodes_support] do
|
|
55
|
+
expect{ src.replace(dst) }.to change{ dst.stat.ino }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'is using temporary file with .tmp extension' do
|
|
59
|
+
expect(src).to receive(:copy).with(having_attributes(extname: '.tmp')).at_least(:once)
|
|
60
|
+
|
|
61
|
+
src.replace(dst)
|
|
62
|
+
end
|
|
42
63
|
end
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
|
|
65
|
+
context 'when src and dst are on same device' do
|
|
66
|
+
before do
|
|
67
|
+
allow_any_instance_of(File::Stat).to receive(:dev).and_return(0)
|
|
68
|
+
end
|
|
46
69
|
|
|
47
|
-
|
|
70
|
+
include_examples 'replaces file'
|
|
71
|
+
end
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
context 'when src and dst are on different devices' do
|
|
74
|
+
before do
|
|
75
|
+
allow_any_instance_of(File::Stat).to receive(:dev, &:__id__)
|
|
76
|
+
end
|
|
50
77
|
|
|
51
|
-
|
|
78
|
+
include_examples 'replaces file'
|
|
52
79
|
end
|
|
53
80
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
81
|
+
context 'when src and dst are on same device, but rename causes Errno::EXDEV' do
|
|
82
|
+
before do
|
|
83
|
+
allow_any_instance_of(File::Stat).to receive(:dev).and_return(0)
|
|
84
|
+
allow(described_class).to receive(:temp_file_path).and_call_original
|
|
85
|
+
expect(described_class).to receive(:temp_file_path).
|
|
86
|
+
with([dst.basename.to_s, '.tmp'], src.dirname).
|
|
87
|
+
and_wrap_original do |m, *args, &block|
|
|
88
|
+
m.call(*args) do |tmp|
|
|
89
|
+
expect(tmp).to receive(:rename).with(dst.to_s).and_raise(Errno::EXDEV)
|
|
90
|
+
block.call(tmp)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
include_examples 'replaces file'
|
|
57
96
|
end
|
|
58
97
|
end
|
|
59
98
|
end
|