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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.appveyor.yml +2 -0
  3. data/.pre-commit-hooks.yaml +9 -0
  4. data/.rubocop.yml +33 -14
  5. data/.travis.yml +17 -15
  6. data/CHANGELOG.markdown +26 -0
  7. data/CONTRIBUTING.markdown +4 -1
  8. data/LICENSE.txt +1 -1
  9. data/README.markdown +9 -3
  10. data/Vagrantfile +1 -1
  11. data/image_optim.gemspec +7 -4
  12. data/lib/image_optim.rb +15 -9
  13. data/lib/image_optim/bin_resolver/bin.rb +10 -8
  14. data/lib/image_optim/bin_resolver/comparable_condition.rb +1 -0
  15. data/lib/image_optim/cache.rb +6 -0
  16. data/lib/image_optim/cache_path.rb +19 -2
  17. data/lib/image_optim/cmd.rb +45 -6
  18. data/lib/image_optim/config.rb +13 -7
  19. data/lib/image_optim/elapsed_time.rb +26 -0
  20. data/lib/image_optim/errors.rb +9 -0
  21. data/lib/image_optim/optimized_path.rb +1 -1
  22. data/lib/image_optim/path.rb +29 -6
  23. data/lib/image_optim/runner/option_parser.rb +6 -0
  24. data/lib/image_optim/timer.rb +25 -0
  25. data/lib/image_optim/worker.rb +29 -26
  26. data/lib/image_optim/worker/advpng.rb +2 -2
  27. data/lib/image_optim/worker/class_methods.rb +2 -0
  28. data/lib/image_optim/worker/gifsicle.rb +3 -3
  29. data/lib/image_optim/worker/jhead.rb +2 -2
  30. data/lib/image_optim/worker/jpegoptim.rb +8 -6
  31. data/lib/image_optim/worker/jpegrecompress.rb +17 -2
  32. data/lib/image_optim/worker/jpegtran.rb +3 -3
  33. data/lib/image_optim/worker/optipng.rb +4 -4
  34. data/lib/image_optim/worker/pngcrush.rb +4 -4
  35. data/lib/image_optim/worker/pngout.rb +2 -2
  36. data/lib/image_optim/worker/pngquant.rb +3 -2
  37. data/lib/image_optim/worker/svgo.rb +2 -2
  38. data/script/update_worker_options_in_readme +1 -1
  39. data/script/worker_analysis +20 -19
  40. data/spec/image_optim/bin_resolver_spec.rb +5 -5
  41. data/spec/image_optim/cache_path_spec.rb +67 -28
  42. data/spec/image_optim/cache_spec.rb +10 -8
  43. data/spec/image_optim/cmd_spec.rb +58 -6
  44. data/spec/image_optim/config_spec.rb +36 -20
  45. data/spec/image_optim/elapsed_time_spec.rb +14 -0
  46. data/spec/image_optim/hash_helpers_spec.rb +18 -18
  47. data/spec/image_optim/option_definition_spec.rb +6 -6
  48. data/spec/image_optim/path_spec.rb +61 -26
  49. data/spec/image_optim/runner/option_parser_spec.rb +4 -4
  50. data/spec/image_optim/timer_spec.rb +32 -0
  51. data/spec/image_optim/worker/jpegrecompress_spec.rb +32 -0
  52. data/spec/image_optim/worker/optipng_spec.rb +11 -11
  53. data/spec/image_optim/worker/pngquant_spec.rb +5 -5
  54. data/spec/image_optim/worker_spec.rb +17 -17
  55. data/spec/image_optim_spec.rb +50 -12
  56. data/spec/images/quant/generate +2 -2
  57. data/spec/spec_helper.rb +16 -15
  58. metadata +36 -12
@@ -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', :rename => 0) }
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', :image_format => :ext)
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', :format => :ext, :basename => '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, :cache_dir => nil, :cache_worker_digests => false)
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
 
@@ -64,6 +64,7 @@ describe ImageOptim::Cache do
64
64
  end
65
65
  end
66
66
 
67
+ # rubocop:disable Style/RedundantFetchBlock
67
68
  shared_examples 'an enabled cache' do
68
69
  context 'when cached file does not exist' do
69
70
  describe :fetch do
@@ -101,7 +102,7 @@ describe ImageOptim::Cache do
101
102
  expect(FileUtils).not_to receive(:mv)
102
103
  expect(File).not_to receive(:rename)
103
104
 
104
- expect(cache.fetch(original){}).to eq(cached)
105
+ expect(cache.fetch(original){ nil }).to eq(cached)
105
106
  end
106
107
 
107
108
  it 'returns nil when file is already optimized' do
@@ -116,11 +117,12 @@ describe ImageOptim::Cache do
116
117
  end
117
118
  end
118
119
  end
120
+ # rubocop:enable Style/RedundantFetchBlock
119
121
 
120
122
  context 'when cache is enabled (without worker digests)' do
121
123
  let(:image_optim) do
122
124
  double(:image_optim,
123
- :cache_dir => cache_dir, :cache_worker_digests => false)
125
+ cache_dir: cache_dir, cache_worker_digests: false, timeout: nil)
124
126
  end
125
127
  let(:cache) do
126
128
  cache = Cache.new(image_optim, {})
@@ -142,8 +144,8 @@ describe ImageOptim::Cache do
142
144
  context 'when cache is enabled (with worker digests)' do
143
145
  let(:image_optim) do
144
146
  double(:image_optim,
145
- :cache_dir => cache_dir,
146
- :cache_worker_digests => true)
147
+ cache_dir: cache_dir,
148
+ cache_worker_digests: true, timeout: nil)
147
149
  end
148
150
  let(:cache) do
149
151
  cache = Cache.new(image_optim, {})
@@ -4,8 +4,6 @@ require 'spec_helper'
4
4
  require 'image_optim/cmd'
5
5
 
6
6
  describe ImageOptim::Cmd do
7
- include CapabilityCheckHelpers
8
-
9
7
  before do
10
8
  stub_const('Cmd', ImageOptim::Cmd)
11
9
  end
@@ -35,12 +33,67 @@ describe ImageOptim::Cmd do
35
33
  expect($CHILD_STATUS.exitstatus).to eq(66)
36
34
  end
37
35
 
38
- it 'raises SignalException if process terminates after signal' do
39
- skip 'signals are not supported' unless signals_supported?
36
+ it 'raises SignalException if process terminates after signal', skip: SkipConditions[:signals_support] do
40
37
  expect_int_exception do
41
38
  Cmd.run('kill -s INT $$')
42
39
  end
43
40
  end
41
+
42
+ context 'with timeout' do
43
+ it 'returns process success status' do
44
+ expect(Cmd.run('sh -c "exit 0"', timeout: 1)).to eq(true)
45
+
46
+ expect(Cmd.run('sh -c "exit 1"', timeout: 1)).to eq(false)
47
+
48
+ expect(Cmd.run('sh -c "exit 66"', timeout: 1)).to eq(false)
49
+ end
50
+
51
+ it 'raises SignalException if process terminates after signal', skip: SkipConditions[:signals_support] do
52
+ expect_int_exception do
53
+ Cmd.run('kill -s INT $$', timeout: 1)
54
+ end
55
+ end
56
+
57
+ it 'raises TimeoutExceeded if process does not exit until timeout' do
58
+ expect do
59
+ Cmd.run('sleep 10', timeout: 0)
60
+ end.to raise_error(ImageOptim::Errors::TimeoutExceeded)
61
+ end
62
+
63
+ it 'does not leave zombie threads' do
64
+ expect do
65
+ begin
66
+ Cmd.run('sleep 10', timeout: 0)
67
+ rescue ImageOptim::Errors::TimeoutExceeded
68
+ # noop
69
+ end
70
+ end.not_to change{ Thread.list }
71
+ end
72
+
73
+ it 'receives TERM', skip: SkipConditions[:signals_support] do
74
+ waiter = double
75
+ allow(Process).to receive(:detach).once{ |pid| @pid = pid; waiter }
76
+ allow(waiter).to receive(:join){ sleep 0.1; nil }
77
+
78
+ expect do
79
+ Cmd.run('sleep 5', timeout: 0.1)
80
+ end.to raise_error(ImageOptim::Errors::TimeoutExceeded)
81
+
82
+ expect(Process.wait2(@pid).last.termsig).to eq(Signal.list['TERM'])
83
+ end
84
+
85
+ it 'receives KILL if it does not react on TERM', skip: SkipConditions[:signals_support] do
86
+ waiter = double
87
+ allow(Process).to receive(:detach).once{ |pid| @pid = pid; waiter }
88
+ allow(waiter).to receive(:join){ sleep 0.1; nil }
89
+
90
+ expect do
91
+ Cmd.run('trap "" TERM; sleep 5', timeout: 0.1)
92
+ end.to raise_error(ImageOptim::Errors::TimeoutExceeded)
93
+
94
+ expect(Process.wait2(@pid).last.termsig).to eq(Signal.list['KILL'])
95
+ end
96
+ end
44
97
  end
45
98
 
46
99
  describe '.capture' do
@@ -62,8 +115,7 @@ describe ImageOptim::Cmd do
62
115
  expect($CHILD_STATUS.exitstatus).to eq(66)
63
116
  end
64
117
 
65
- it 'raises SignalException if process terminates after signal' do
66
- skip 'signals are not supported' unless signals_supported?
118
+ it 'raises SignalException if process terminates after signal', skip: SkipConditions[:signals_support] do
67
119
  expect_int_exception do
68
120
  Cmd.capture('kill -s INT $$')
69
121
  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(:unused => true)
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(:nice => false)
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(:nice => '13')
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(:threads => false)
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(:threads => '616')
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(:cache_dir => '')
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(:abc => {:option => true})
116
- expect(config.for_worker(Abc)).to eq(:option => true)
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(:abc => false)
121
- expect(config.for_worker(Abc)).to eq(:disable => true)
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(:abc => 13)
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(:a => 1, :b => 2, :c => 3)
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(:a => 10, :b => 20)
153
+ with(IOConfig::LOCAL_PATH).and_return(a: 10, b: 20)
138
154
 
139
- config = IOConfig.new(:a => 100)
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(:config_paths => [])
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(:a => 1, :b => 2, :c => 3)
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(:a => 10, :b => 20)
173
+ with('config/image_optim.yml').and_return(a: 10, b: 20)
158
174
 
159
- config = IOConfig.new(:a => 100, :config_paths => %w[
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(:config_paths => 'config/image_optim.yml')
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 = {:config => {:this => true}}
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
@@ -10,10 +10,10 @@ describe ImageOptim::HashHelpers do
10
10
 
11
11
  context 'stringify/simbolyze' do
12
12
  symbol_keys = {
13
- :a => 1,
14
- :b => {
15
- :c => [:a, 'a'],
16
- :d => {},
13
+ a: 1,
14
+ b: {
15
+ c: [:a, 'a'],
16
+ d: {},
17
17
  },
18
18
  }
19
19
 
@@ -38,22 +38,22 @@ describe ImageOptim::HashHelpers do
38
38
 
39
39
  it 'deep merges hashes' do
40
40
  merge_a = {
41
- :a => {
42
- :b => 1,
43
- :c => {
44
- :d => 2,
45
- :e => {:f => true},
41
+ a: {
42
+ b: 1,
43
+ c: {
44
+ d: 2,
45
+ e: {f: true},
46
46
  },
47
47
  },
48
- :y => 10,
48
+ y: 10,
49
49
  }
50
50
 
51
51
  merge_b = {
52
52
  :a => {
53
- :b => 2,
54
- :c => {
55
- :d => 3,
56
- :e => false,
53
+ b: 2,
54
+ c: {
55
+ d: 3,
56
+ e: false,
57
57
  },
58
58
  },
59
59
  'z' => 20,
@@ -61,10 +61,10 @@ describe ImageOptim::HashHelpers do
61
61
 
62
62
  merge_result = {
63
63
  :a => {
64
- :b => 2,
65
- :c => {
66
- :d => 3,
67
- :e => false,
64
+ b: 2,
65
+ c: {
66
+ d: 3,
67
+ e: false,
68
68
  },
69
69
  },
70
70
  :y => 10,
@@ -58,13 +58,13 @@ describe ImageOptim::OptionDefinition do
58
58
 
59
59
  context 'when option is nil' do
60
60
  it 'returns nil' do
61
- expect(subject.value(nil, :abc => nil)).to eq(nil)
61
+ expect(subject.value(nil, abc: nil)).to eq(nil)
62
62
  end
63
63
  end
64
64
 
65
65
  context 'when option is set' do
66
66
  it 'returns value' do
67
- expect(subject.value(nil, :abc => 123)).to eq(123)
67
+ expect(subject.value(nil, abc: 123)).to eq(123)
68
68
  end
69
69
  end
70
70
  end
@@ -84,13 +84,13 @@ describe ImageOptim::OptionDefinition do
84
84
 
85
85
  context 'when option is nil' do
86
86
  it 'returns nil passed through proc' do
87
- expect(subject.value(nil, :abc => nil)).to eq('nil')
87
+ expect(subject.value(nil, abc: nil)).to eq('nil')
88
88
  end
89
89
  end
90
90
 
91
91
  context 'when option is set' do
92
92
  it 'returns value passed through proc' do
93
- expect(subject.value(nil, :abc => 123)).to eq('123')
93
+ expect(subject.value(nil, abc: 123)).to eq('123')
94
94
  end
95
95
  end
96
96
  end
@@ -108,13 +108,13 @@ describe ImageOptim::OptionDefinition do
108
108
 
109
109
  context 'when option is nil' do
110
110
  it 'returns nil passed through proc' do
111
- expect(subject.value(nil, :abc => nil)).to eq(['nil', subject])
111
+ expect(subject.value(nil, abc: nil)).to eq(['nil', subject])
112
112
  end
113
113
  end
114
114
 
115
115
  context 'when option is set' do
116
116
  it 'returns value passed through proc' do
117
- expect(subject.value(nil, :abc => 123)).to eq(['123', subject])
117
+ expect(subject.value(nil, abc: 123)).to eq(['123', subject])
118
118
  end
119
119
  end
120
120
  end
@@ -5,8 +5,6 @@ require 'image_optim/path'
5
5
  require 'tempfile'
6
6
 
7
7
  describe ImageOptim::Path do
8
- include CapabilityCheckHelpers
9
-
10
8
  before do
11
9
  stub_const('Path', ImageOptim::Path)
12
10
  end
@@ -61,45 +59,82 @@ describe ImageOptim::Path do
61
59
  let(:src){ Path.temp_file_path }
62
60
  let(:dst){ Path.temp_file_path }
63
61
 
64
- it 'moves data to destination' do
65
- src.write('src')
62
+ shared_examples 'replaces file' do
63
+ it 'moves data to destination' do
64
+ src.write('src')
66
65
 
67
- src.replace(dst)
66
+ src.replace(dst)
68
67
 
69
- expect(dst.read).to eq('src')
70
- end
68
+ expect(dst.read).to eq('src')
69
+ end
71
70
 
72
- it 'removes original file' do
73
- src.replace(dst)
71
+ it 'removes original file' do
72
+ src.replace(dst)
74
73
 
75
- expect(src).to_not exist
76
- end
74
+ expect(src).to_not exist
75
+ end
76
+
77
+ it 'preserves attributes of destination file', skip: SkipConditions[:any_file_mode_allowed] do
78
+ mode = 0o666
79
+
80
+ dst.chmod(mode)
81
+
82
+ src.replace(dst)
83
+
84
+ got = dst.stat.mode & 0o777
85
+ expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
86
+ end
87
+
88
+ it 'does not preserve mtime of destination file' do
89
+ time = src.mtime
90
+
91
+ dst.utime(time - 1000, time - 1000)
77
92
 
78
- it 'preserves attributes of destination file' do
79
- skip 'full file modes are not support' unless any_file_modes_allowed?
80
- mode = 0o666
93
+ src.replace(dst)
81
94
 
82
- dst.chmod(mode)
95
+ expect(dst.mtime).to be >= time
96
+ end
97
+
98
+ it 'changes inode of destination', skip: SkipConditions[:inodes_support] do
99
+ expect{ src.replace(dst) }.to change{ dst.stat.ino }
100
+ end
101
+ end
83
102
 
84
- src.replace(dst)
103
+ context 'when src and dst are on same device' do
104
+ before do
105
+ allow_any_instance_of(File::Stat).to receive(:dev).and_return(0)
106
+ end
85
107
 
86
- got = dst.stat.mode & 0o777
87
- expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
108
+ include_examples 'replaces file'
88
109
  end
89
110
 
90
- it 'does not preserve mtime of destination file' do
91
- time = src.mtime
111
+ context 'when src and dst are on different devices' do
112
+ before do
113
+ allow_any_instance_of(File::Stat).to receive(:dev, &:__id__)
114
+ end
92
115
 
93
- dst.utime(time - 1000, time - 1000)
116
+ include_examples 'replaces file'
94
117
 
95
- src.replace(dst)
118
+ it 'is using temporary file with .tmp extension' do
119
+ expect(src).to receive(:move).with(having_attributes(extname: '.tmp'))
96
120
 
97
- expect(dst.mtime).to be >= time
121
+ src.replace(dst)
122
+ end
98
123
  end
99
124
 
100
- it 'changes inode of destination' do
101
- skip 'inodes are not supported' unless inodes_supported?
102
- expect{ src.replace(dst) }.to change{ dst.stat.ino }
125
+ context 'when src and dst are on same device, but rename causes Errno::EXDEV' do
126
+ before do
127
+ allow_any_instance_of(File::Stat).to receive(:dev).and_return(0)
128
+ expect(src).to receive(:rename).with(dst.to_s).once.and_raise(Errno::EXDEV)
129
+ end
130
+
131
+ include_examples 'replaces file'
132
+
133
+ it 'is using temporary file with .tmp extension' do
134
+ expect(src).to receive(:move).with(having_attributes(extname: '.tmp'))
135
+
136
+ src.replace(dst)
137
+ end
103
138
  end
104
139
  end
105
140
  end