sugar_utils 0.5.0 → 0.6.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.
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SugarUtils
4
+ module File
5
+ # @api private
6
+ class WriteOptions
7
+ # @parma filename [String]
8
+ # @param options [Hash]
9
+ def initialize(filename, options)
10
+ @filename = filename
11
+ @options = options
12
+
13
+ return unless filename && ::File.exist?(filename)
14
+
15
+ file_stat = ::File::Stat.new(filename)
16
+ @existing_owner = file_stat.uid
17
+ @existing_group = file_stat.gid
18
+ end
19
+
20
+ # @return [Boolean]
21
+ def flush?
22
+ @options[:flush] || false
23
+ end
24
+
25
+ # @overload perm
26
+ # The default permission is 0o644
27
+ # @overload perm(default_value)
28
+ # @param default_value [nil, Integer]
29
+ # Override the default_value including allowing nil.
30
+ #
31
+ # @return [Integer]
32
+ def perm(default_value = 0o644)
33
+ # NOTE: We are using the variable name 'perm' because that is the name
34
+ # of the argument used by File.open.
35
+ @options[:mode] || @options[:perm] || default_value
36
+ end
37
+
38
+ # @return [String, Integer]
39
+ def owner
40
+ @options[:owner] || @existing_owner
41
+ end
42
+
43
+ # @return [String, Intekuuger]
44
+ def group
45
+ @options[:group] || @existing_group
46
+ end
47
+
48
+ # @param keys [Array]
49
+ # @return [Hash]
50
+ def slice(*args)
51
+ keys = args.flatten.compact
52
+ @options.select { |k| keys.include?(k) }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,6 +1,5 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module SugarUtils
5
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'.freeze
6
5
  end
@@ -1,10 +1,9 @@
1
- # encoding : utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
5
- require 'sugar_utils'
3
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
6
4
  require 'rspec/tabular'
7
5
  require 'fakefs/spec_helpers'
6
+ require 'rspec/side_effects'
8
7
  require 'etc'
9
8
  # HACK: including pp seems to resolve an error with FakeFS and File.read
10
9
  # This seems to be related to but not the same as the problem mentioned in the
@@ -16,6 +15,9 @@ require 'pp'
16
15
  require 'simplecov'
17
16
  SimpleCov.start
18
17
 
18
+ require 'sugar_utils'
19
+ MultiJson.use(:ok_json)
20
+
19
21
  SolidAssert.enable_assertions
20
22
 
21
23
  RSpec.configure do |config|
@@ -48,8 +50,8 @@ RSpec::Matchers.define :have_file_permission do |expected|
48
50
  match do |actual|
49
51
  next false unless File.exist?(actual)
50
52
 
51
- @actual = format('%o', File.stat(actual).mode)
52
- @expected = format('%o', expected)
53
+ @actual = format('%<mode>o', mode: File.stat(actual).mode)
54
+ @expected = format('%<mode>o', mode: expected)
53
55
  values_match?(@expected, @actual)
54
56
  end
55
57
  end
@@ -78,7 +80,7 @@ RSpec::Matchers.define :have_mtime do |expected|
78
80
  match do |actual|
79
81
  next false unless File.exist?(actual)
80
82
 
81
- @actual = File.stat(actual).mtime
83
+ @actual = File.stat(actual).mtime.to_i
82
84
  @expected = expected
83
85
  values_match?(@expected, @actual)
84
86
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe SugarUtils::File::WriteOptions do
6
+ subject(:write_options) { described_class.new(filename, options) }
7
+
8
+ let(:filename) { nil }
9
+
10
+ before do
11
+ allow(File).to receive(:exist?).with('missing').and_return(false)
12
+ allow(File).to receive(:exist?).with('found').and_return(true)
13
+ allow(File::Stat).to receive(:new).with('found').and_return(
14
+ instance_double(File::Stat, uid: :uid, gid: :gid)
15
+ )
16
+ end
17
+
18
+ describe '#flush?' do
19
+ subject { write_options.flush? }
20
+
21
+ inputs :options
22
+ it_with Hash[], false
23
+ it_with Hash[flush: :flush], :flush
24
+ end
25
+
26
+ describe '#perm' do
27
+ subject { write_options.perm(*args) }
28
+
29
+ inputs :options, :args
30
+ it_with Hash[], [], 0o644
31
+ it_with Hash[], %i[default_value], :default_value
32
+ it_with Hash[mode: :mode], [], :mode
33
+ it_with Hash[mode: :mode], %i[default_value], :mode
34
+ it_with Hash[perm: :perm, mode: :mode], [], :mode
35
+ it_with Hash[perm: :perm, mode: :mode], %i[default_value], :mode
36
+ it_with Hash[perm: :perm], [], :perm
37
+ it_with Hash[perm: :perm], %i[default_value], :perm
38
+ end
39
+
40
+ describe '#owner' do
41
+ subject { write_options.owner }
42
+
43
+ inputs :filename, :options
44
+ it_with nil, Hash[], nil
45
+ it_with 'missing', Hash[], nil
46
+ it_with 'found', Hash[], :uid
47
+ it_with nil, Hash[owner: :owner], :owner
48
+ it_with 'missing', Hash[owner: :owner], :owner
49
+ it_with 'found', Hash[owner: :owner], :owner
50
+ end
51
+
52
+ describe '#group' do
53
+ subject { write_options.group }
54
+
55
+ inputs :filename, :options
56
+ it_with nil, Hash[], nil
57
+ it_with 'missing', Hash[], nil
58
+ it_with 'found', Hash[], :gid
59
+ it_with nil, Hash[group: :group], :group
60
+ it_with 'missing', Hash[group: :group], :group
61
+ it_with 'found', Hash[group: :group], :group
62
+ end
63
+
64
+ describe '#slice' do
65
+ subject { write_options.slice(*args) }
66
+
67
+ let(:options) { { key1: :value1, key2: :value2, key3: :value3 } }
68
+
69
+ inputs :args
70
+ it_with [], Hash[]
71
+ it_with %i[key1], Hash[key1: :value1]
72
+ it_with %i[key2], Hash[key2: :value2]
73
+ it_with %i[key3], Hash[key3: :value3]
74
+ it_with %i[key1 key3], Hash[key1: :value1, key3: :value3]
75
+ it_with [%i[key1], nil, %i[key3]], Hash[key1: :value1, key3: :value3]
76
+ end
77
+ end
@@ -1,4 +1,3 @@
1
- # encoding : utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'spec_helper'
@@ -6,7 +5,9 @@ require 'spec_helper'
6
5
  describe SugarUtils::File do
7
6
  describe '.flock_shared' do
8
7
  subject { described_class.flock_shared(file, options) }
8
+
9
9
  let(:file) { instance_double(File) }
10
+
10
11
  before do
11
12
  allow(Timeout).to receive(:timeout).with(expected_timeout).and_yield
12
13
  expect(file).to receive(:flock).with(::File::LOCK_SH)
@@ -20,7 +21,9 @@ describe SugarUtils::File do
20
21
 
21
22
  describe '.flock_exclusive' do
22
23
  subject { described_class.flock_exclusive(file, options) }
24
+
23
25
  let(:file) { instance_double(File) }
26
+
24
27
  before do
25
28
  allow(Timeout).to receive(:timeout).with(expected_timeout).and_yield
26
29
  expect(file).to receive(:flock).with(::File::LOCK_EX)
@@ -32,6 +35,65 @@ describe SugarUtils::File do
32
35
  side_effects_with Hash[timeout: 5], 5
33
36
  end
34
37
 
38
+ describe '.change_access', :fakefs do
39
+ subject do
40
+ described_class.change_access(filename, owner, group, permission)
41
+ end
42
+
43
+ let(:filename) { 'filename' }
44
+
45
+ context 'when file does not exist' do
46
+ let(:owner) { 'nobody' }
47
+ let(:group) { 'nogroup' }
48
+ let(:permission) { 0o777 }
49
+
50
+ it { expect_raise_error("Unable to change access on #{filename}") }
51
+ end
52
+
53
+ context 'when file exists' do
54
+ before { write(filename, 'foobar') }
55
+
56
+ context 'with no values specified' do # rubocop:disable RSpec/NestedGroups
57
+ let(:owner) { nil }
58
+ let(:group) { nil }
59
+ let(:permission) { nil }
60
+
61
+ it { expect_not_to_raise_error }
62
+ its_side_effects_are do
63
+ expect(filename).not_to have_owner('nobody')
64
+ expect(filename).not_to have_group('nogroup')
65
+ expect(filename).not_to have_file_permission(0o100777)
66
+ end
67
+ end
68
+
69
+ context 'with all values(Integer) specified' do # rubocop:disable RSpec/NestedGroups
70
+ let(:owner) { Etc.getpwnam('nobody').uid }
71
+ let(:group) { Etc.getgrnam('nogroup').gid }
72
+ let(:permission) { 0o777 }
73
+
74
+ it { expect_not_to_raise_error }
75
+ its_side_effects_are do
76
+ expect(filename).to have_owner('nobody')
77
+ expect(filename).to have_group('nogroup')
78
+ expect(filename).to have_file_permission(0o100777)
79
+ end
80
+ end
81
+
82
+ context 'with all values specified' do # rubocop:disable RSpec/NestedGroups
83
+ let(:owner) { 'nobody' }
84
+ let(:group) { 'nogroup' }
85
+ let(:permission) { 0o777 }
86
+
87
+ it { expect_not_to_raise_error }
88
+ its_side_effects_are do
89
+ expect(filename).to have_owner('nobody')
90
+ expect(filename).to have_group('nogroup')
91
+ expect(filename).to have_file_permission(0o100777)
92
+ end
93
+ end
94
+ end
95
+ end
96
+
35
97
  describe '.read', :fakefs do
36
98
  subject { described_class.read('filename', options) }
37
99
 
@@ -45,43 +107,49 @@ describe SugarUtils::File do
45
107
  it_with Hash[raise_on_missing: false, value_on_missing: 'hi'], 'hi'
46
108
  end
47
109
 
48
- context 'missing file' do
110
+ context 'when missing file' do
49
111
  it_behaves_like 'handles the missing file error'
50
112
  end
51
113
 
52
114
  context 'with IOError' do
53
115
  before { allow(File).to receive(:open).and_raise(IOError) }
116
+
54
117
  it_behaves_like 'handles the missing file error'
55
118
  end
56
119
 
57
- context 'file present' do
58
- before { write('filename', "foo\x92bar") }
120
+ context 'when file present' do
121
+ before { write('filename', 'foobar') }
59
122
 
60
- context 'and locked' do
123
+ # rubocop:disable RSpec/NestedGroups
124
+ context 'when locked' do
61
125
  let(:options) { { key: :value } }
126
+
62
127
  before do
63
128
  expect(described_class).to receive(:flock_shared)
64
129
  .with(kind_of(File), options)
65
130
  .and_raise(Timeout::Error)
66
131
  end
132
+
67
133
  it { expect_raise_error('Cannot read filename because it is locked') }
68
134
  end
69
135
 
70
- context 'and unlocked' do
136
+ context 'when unlocked' do
71
137
  let(:options) { { key: :value, scrub_encoding: scrub_encoding } }
138
+
72
139
  before do
73
140
  expect(described_class).to receive(:flock_shared)
74
141
  .with(kind_of(File), options)
142
+ allow(SugarUtils).to receive(:scrub_encoding)
143
+ .with('foobar', scrub_encoding)
144
+ .and_return(:scrubbed_data)
75
145
  end
76
146
 
77
147
  inputs :scrub_encoding
78
- it_with nil, "foo\x92bar"
79
- it_with false, "foo\x92bar"
80
- it_with true, 'foobar'
81
- it_with '', 'foobar'
82
- it_with 'x', 'fooxbar'
83
- it_with 'xxx', 'fooxxxbar'
148
+ it_with nil, 'foobar'
149
+ it_with false, 'foobar'
150
+ it_with :scrub_encoding, :scrubbed_data
84
151
  end
152
+ # rubocop:enable RSpec/NestedGroups
85
153
  end
86
154
  end
87
155
 
@@ -110,109 +178,241 @@ describe SugarUtils::File do
110
178
 
111
179
  let(:filename) { 'path1/path2/filename' }
112
180
 
113
- before { subject }
114
-
115
- inputs :options # rubocop:disable ExtraSpacing, SpaceBeforeFirstArg
116
- specify_with([]) { expect(File.exist?(filename)).to eq(true) }
117
- specify_with([{ owner: 'nobody' }]) { expect(filename).to have_owner('nobody') }
118
- specify_with([{ group: 'nogroup' }]) { expect(filename).to have_group('nogroup') }
119
- specify_with([{ mode: 0o600 }]) { expect(filename).to have_file_permission(0o100600) }
120
- specify_with([{ perm: 0o600 }]) { expect(filename).to have_file_permission(0o100600) }
121
- specify_with([{ mtime: 0 }]) { expect(filename).to have_mtime(0) }
122
- specify_with([{ owner: 'nobody', group: 'nogroup', mode: 0o600, mtime: 0 }]) do
123
- expect(filename).to have_owner('nobody')
124
- expect(filename).to have_group('nogroup')
125
- expect(filename).to have_file_permission(0o100600)
126
- expect(filename).to have_mtime(0)
181
+ context 'without options' do
182
+ let(:options) { [] }
183
+
184
+ it { expect_not_to_raise_error }
185
+ its_side_effects_are { expect(File.exist?(filename)).to eq(true) }
186
+ end
187
+
188
+ context 'with options, and :mode key' do
189
+ let(:options) { [{ owner: 'nobody', group: 'nogroup', mode: 0o600, mtime: 0 }] }
190
+
191
+ it { expect_not_to_raise_error }
192
+ its_side_effects_are do
193
+ expect(filename).to have_owner('nobody')
194
+ expect(filename).to have_group('nogroup')
195
+ expect(filename).to have_file_permission(0o100600)
196
+ expect(filename).to have_mtime(0)
197
+ end
198
+ end
199
+
200
+ context 'with options, and :perm key' do
201
+ let(:options) { [{ owner: 'nobody', group: 'nogroup', perm: 0o600, mtime: 0 }] }
202
+
203
+ it { expect_not_to_raise_error }
204
+ its_side_effects_are do
205
+ expect(filename).to have_owner('nobody')
206
+ expect(filename).to have_group('nogroup')
207
+ expect(filename).to have_file_permission(0o100600)
208
+ expect(filename).to have_mtime(0)
209
+ end
127
210
  end
128
211
  end
129
212
 
130
213
  describe '.write', :fakefs do
131
214
  subject { described_class.write(filename, data, options) }
215
+
132
216
  let(:data) { 'content' }
133
217
  let(:filename) { 'dir1/dir2/filename' }
134
218
 
135
- context 'SystemCallError' do
219
+ context 'when SystemCallError' do
136
220
  let(:options) { {} }
137
221
  let(:exception) { SystemCallError.new(nil) }
222
+
138
223
  before { allow(File).to receive(:open).and_raise(exception) }
224
+
139
225
  it { expect_raise_error("Unable to write #{filename} with #{exception}") }
140
226
  end
141
227
 
142
- context 'IOError' do
228
+ context 'when IOError' do
143
229
  let(:options) { {} }
144
230
  let(:exception) { IOError.new(nil) }
231
+
145
232
  before { allow(File).to receive(:open).and_raise(exception) }
233
+
146
234
  it { expect_raise_error("Unable to write #{filename} with #{exception}") }
147
235
  end
148
236
 
149
- context 'locked' do
237
+ context 'when locked' do
150
238
  let(:options) { {} }
239
+
151
240
  before do
152
241
  expect(described_class).to receive(:flock_exclusive)
153
242
  .with(kind_of(File), options)
154
243
  .and_raise(Timeout::Error)
155
244
  end
245
+
156
246
  it { expect_raise_error("Unable to write #{filename} because it is locked") }
157
247
  end
158
248
 
159
- context 'unlocked' do
160
- shared_examples_for 'file is written' do
161
- before do
162
- expect(described_class).to receive(:flock_exclusive)
163
- .with(kind_of(File), options)
249
+ shared_examples_for 'file is correctly written' do
250
+ before do
251
+ expect(described_class).to receive(:flock_exclusive)
252
+ .with(kind_of(File), options)
253
+ end
254
+
255
+ # rubocop:disable RSpec/NestedGroups
256
+ context 'without options' do
257
+ let(:options) { {} }
258
+
259
+ it { expect_not_to_raise_error }
260
+ its_side_effects_are do
261
+ expect(filename).to have_content(data)
262
+ expect(filename).to have_file_permission(0o100644)
164
263
  end
264
+ end
165
265
 
166
- context 'default options' do
167
- let(:options) { {} }
168
- before { subject }
169
- specify { expect(filename).to have_content(data) }
170
- specify { expect(filename).to have_file_permission(0o100644) }
266
+ context 'with options' do
267
+ let(:options) do
268
+ { flush: true, owner: 'nobody', group: 'nogroup', mode_or_perm_key => 0o600 }
171
269
  end
172
270
 
173
- context 'with deprecated options' do
174
- let(:options) { { mode: 0o600 } }
175
- before { subject }
176
- specify { expect(filename).to have_content(data) }
177
- specify { expect(filename).to have_file_permission(0o100600) }
271
+ before do
272
+ # rubocop:disable RSpec/AnyInstance
273
+ expect_any_instance_of(File).to receive(:flush)
274
+ expect_any_instance_of(File).to receive(:fsync)
275
+ # rubocop:enable RSpec/AnyInstance
178
276
  end
179
277
 
180
- context 'without deprecated options' do
181
- let(:options) do
182
- { flush: true, owner: 'nobody', group: 'nogroup', mode: 'w', perm: 0o600 }
278
+ context 'with mode key' do
279
+ let(:mode_or_perm_key) { :mode }
280
+
281
+ it { expect_not_to_raise_error }
282
+ its_side_effects_are do
283
+ expect(filename).to have_content(data)
284
+ expect(filename).to have_owner('nobody')
285
+ expect(filename).to have_group('nogroup')
286
+ expect(filename).to have_file_permission(0o100600)
183
287
  end
184
- before do
185
- expect_any_instance_of(File).to receive(:flush)
186
- expect_any_instance_of(File).to receive(:fsync)
187
- subject
288
+ end
289
+
290
+ context 'with perm key' do
291
+ let(:mode_or_perm_key) { :perm }
292
+
293
+ it { expect_not_to_raise_error }
294
+ its_side_effects_are do
295
+ expect(filename).to have_content(data)
296
+ expect(filename).to have_owner('nobody')
297
+ expect(filename).to have_group('nogroup')
298
+ expect(filename).to have_file_permission(0o100600)
188
299
  end
189
- specify { expect(filename).to have_content(data) }
190
- specify { expect(filename).to have_owner('nobody') }
191
- specify { expect(filename).to have_group('nogroup') }
192
- specify { expect(filename).to have_file_permission(0o100600) }
193
300
  end
194
301
  end
302
+ # rubocop:enable RSpec/NestedGroups
303
+ end
304
+
305
+ context 'when file does not exist' do
306
+ it_behaves_like 'file is correctly written'
307
+ end
308
+
309
+ context 'when file exists' do
310
+ before { write(filename, 'foobar', 0o777) }
311
+
312
+ it_behaves_like 'file is correctly written'
313
+ end
314
+ end
315
+
316
+ describe '.atomic_write', :fakefs do
317
+ subject { described_class.atomic_write(filename, data, options) }
318
+
319
+ let(:data) { 'content' }
320
+ let(:filename) { 'dir1/dir2/filename' }
321
+
322
+ context 'when SystemCallError' do
323
+ let(:options) { {} }
324
+ let(:exception) { SystemCallError.new(nil) }
325
+
326
+ before { allow(File).to receive(:open).and_raise(exception) }
327
+
328
+ it { expect_raise_error("Unable to write #{filename} with #{exception}") }
329
+ end
330
+
331
+ context 'when IOError' do
332
+ let(:options) { {} }
333
+ let(:exception) { IOError.new(nil) }
195
334
 
196
- context 'and not exist' do
197
- it_behaves_like 'file is written'
335
+ before { allow(File).to receive(:open).and_raise(exception) }
336
+
337
+ it { expect_raise_error("Unable to write #{filename} with #{exception}") }
338
+ end
339
+
340
+ context 'when locked' do
341
+ let(:options) { {} }
342
+
343
+ before do
344
+ expect(described_class).to receive(:flock_exclusive)
345
+ .with(kind_of(File), options)
346
+ .and_raise(Timeout::Error)
198
347
  end
199
348
 
200
- context 'and exists' do
201
- before { write(filename, 'foobar', 0o777) }
202
- context 'not locked' do
203
- it_behaves_like 'file is written'
349
+ it { expect_raise_error("Unable to write #{filename} because it is locked") }
350
+ end
351
+
352
+ shared_examples_for 'file is correctly written' do
353
+ before do
354
+ expect(described_class).to receive(:flock_exclusive)
355
+ .with(kind_of(File), options)
356
+ end
357
+
358
+ # rubocop:disable RSpec/NestedGroups
359
+ context 'without options' do
360
+ let(:options) { {} }
361
+
362
+ it { expect_not_to_raise_error }
363
+ its_side_effects_are do
364
+ expect(filename).to have_content(data)
365
+ expect(filename).to have_file_permission(0o100644)
366
+ end
367
+ end
204
368
 
205
- context 'with append mode' do
206
- let(:options) { { mode: 'a+' } }
207
- before do
208
- expect(described_class).to receive(:flock_exclusive)
209
- .with(kind_of(File), options)
210
- subject
211
- end
212
- specify { expect(filename).to have_content("foobar#{data}") }
369
+ context 'with options' do
370
+ let(:options) do
371
+ { flush: true, owner: 'nobody', group: 'nogroup', mode_or_perm_key => 0o600 }
372
+ end
373
+
374
+ before do
375
+ # rubocop:disable RSpec/AnyInstance
376
+ expect_any_instance_of(File).to receive(:flush)
377
+ expect_any_instance_of(File).to receive(:fsync)
378
+ # rubocop:enable RSpec/AnyInstance
379
+ end
380
+
381
+ context 'with mode key' do
382
+ let(:mode_or_perm_key) { :mode }
383
+
384
+ it { expect_not_to_raise_error }
385
+ its_side_effects_are do
386
+ expect(filename).to have_content(data)
387
+ expect(filename).to have_owner('nobody')
388
+ expect(filename).to have_group('nogroup')
389
+ expect(filename).to have_file_permission(0o100600)
390
+ end
391
+ end
392
+
393
+ context 'with perm key' do
394
+ let(:mode_or_perm_key) { :perm }
395
+
396
+ it { expect_not_to_raise_error }
397
+ its_side_effects_are do
398
+ expect(filename).to have_content(data)
399
+ expect(filename).to have_owner('nobody')
400
+ expect(filename).to have_group('nogroup')
401
+ expect(filename).to have_file_permission(0o100600)
213
402
  end
214
403
  end
215
404
  end
405
+ # rubocop:enable RSpec/NestedGroups
406
+ end
407
+
408
+ context 'when file does not exist' do
409
+ it_behaves_like 'file is correctly written'
410
+ end
411
+
412
+ context 'when file exists' do
413
+ before { write(filename, 'foobar', 0o777) }
414
+
415
+ it_behaves_like 'file is correctly written'
216
416
  end
217
417
  end
218
418
 
@@ -220,30 +420,142 @@ describe SugarUtils::File do
220
420
  subject { described_class.write_json(:filename, data, :options) }
221
421
 
222
422
  let(:data) { { 'key' => 'value' } }
423
+
223
424
  before do
224
- expect(described_class).to receive(:write).with(
425
+ expect(described_class).to receive(:atomic_write).with(
225
426
  :filename, MultiJson.dump(data, pretty: true), :options
226
427
  )
227
428
  end
228
429
 
229
- specify { subject }
430
+ it_has_side_effects
431
+ end
432
+
433
+ describe '.append', :fakefs do
434
+ subject { described_class.append(filename, data, options) }
435
+
436
+ let(:data) { 'content' }
437
+ let(:filename) { 'dir1/dir2/filename' }
438
+
439
+ context 'when SystemCallError' do
440
+ let(:options) { {} }
441
+ let(:exception) { SystemCallError.new(nil) }
442
+
443
+ before { allow(File).to receive(:open).and_raise(exception) }
444
+
445
+ it { expect_raise_error("Unable to write #{filename} with #{exception}") }
446
+ end
447
+
448
+ context 'when IOError' do
449
+ let(:options) { {} }
450
+ let(:exception) { IOError.new(nil) }
451
+
452
+ before { allow(File).to receive(:open).and_raise(exception) }
453
+
454
+ it { expect_raise_error("Unable to write #{filename} with #{exception}") }
455
+ end
456
+
457
+ context 'when locked' do
458
+ let(:options) { {} }
459
+
460
+ before do
461
+ expect(described_class).to receive(:flock_exclusive)
462
+ .with(kind_of(File), options)
463
+ .and_raise(Timeout::Error)
464
+ end
465
+
466
+ it { expect_raise_error("Unable to write #{filename} because it is locked") }
467
+ end
468
+
469
+ shared_examples_for 'file is correctly appended' do
470
+ before do
471
+ expect(described_class).to receive(:flock_exclusive)
472
+ .with(kind_of(File), options)
473
+ end
474
+
475
+ # rubocop:disable RSpec/NestedGroups
476
+ context 'without options' do
477
+ let(:options) { {} }
478
+
479
+ it { expect_not_to_raise_error }
480
+ its_side_effects_are do
481
+ expect(filename).to have_content(expected_file_data)
482
+ expect(filename).to have_file_permission(0o100644)
483
+ end
484
+ end
485
+
486
+ context 'with options' do
487
+ let(:options) do
488
+ { flush: true, owner: 'nobody', group: 'nogroup', mode_or_perm_key => 0o600 }
489
+ end
490
+
491
+ before do
492
+ # rubocop:disable RSpec/AnyInstance
493
+ expect_any_instance_of(File).to receive(:flush)
494
+ expect_any_instance_of(File).to receive(:fsync)
495
+ # rubocop:enable RSpec/AnyInstance
496
+ end
497
+
498
+ context 'with mode key' do
499
+ let(:mode_or_perm_key) { :mode }
500
+
501
+ it { expect_not_to_raise_error }
502
+ its_side_effects_are do
503
+ expect(filename).to have_content(expected_file_data)
504
+ expect(filename).to have_owner('nobody')
505
+ expect(filename).to have_group('nogroup')
506
+ expect(filename).to have_file_permission(0o100600)
507
+ end
508
+ end
509
+
510
+ context 'with perm key' do
511
+ let(:mode_or_perm_key) { :perm }
512
+
513
+ it { expect_not_to_raise_error }
514
+ its_side_effects_are do
515
+ expect(filename).to have_content(expected_file_data)
516
+ expect(filename).to have_owner('nobody')
517
+ expect(filename).to have_group('nogroup')
518
+ expect(filename).to have_file_permission(0o100600)
519
+ end
520
+ end
521
+ end
522
+ # rubocop:enable RSpec/NestedGroups
523
+ end
524
+
525
+ context 'when file does not exist' do
526
+ let(:expected_file_data) { data }
527
+
528
+ it_behaves_like 'file is correctly appended'
529
+ end
530
+
531
+ context 'when file exists' do
532
+ let(:expected_file_data) { "foobar#{data}" }
533
+
534
+ before { write(filename, 'foobar', 0o777) }
535
+
536
+ it_behaves_like 'file is correctly appended'
537
+ end
230
538
  end
231
539
 
232
540
  ##############################################################################
233
541
 
234
- # @param [String] message
542
+ # @param message [String]
235
543
  def expect_raise_error(message)
236
544
  expect { subject }.to raise_error(described_class::Error, message)
237
545
  end
238
546
 
547
+ def expect_not_to_raise_error
548
+ expect { subject }.not_to raise_error
549
+ end
550
+
239
551
  # @overload write(filename, content)
240
- # @param [String] filename
241
- # @param [String] content
552
+ # @param filename [String]
553
+ # @param content [String]
242
554
  #
243
555
  # @overload write(filename, content, perm)
244
- # @param [String] filename
245
- # @param [String] content
246
- # @param [Integer] perm
556
+ # @param filename [String]
557
+ # @param content [String]
558
+ # @param perm [Integer]
247
559
  #
248
560
  # @return [void]
249
561
  def write(filename, content, perm = nil)