file_overwrite 0.1

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,4 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class FileOverwriteError < StandardError
4
+ end
@@ -0,0 +1,469 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Author: M. Sakano (Wise Babel Ltd)
4
+ # License: MIT
5
+
6
+ # require 'tempfile'
7
+ # require 'fileutils'
8
+ require 'file_overwrite/file_overwrite'
9
+
10
+ $stdout.sync=true
11
+ $stderr.sync=true
12
+ # print '$LOAD_PATH=';p $LOAD_PATH
13
+
14
+ #################################################
15
+ # Unit Test
16
+ #################################################
17
+
18
+ #if $0 == __FILE__
19
+ gem "minitest"
20
+ # require 'minitest/unit'
21
+ require 'minitest/autorun'
22
+ # MiniTest::Unit.autorun
23
+
24
+ class TestUnitFileOverwrite < MiniTest::Test
25
+ T = true
26
+ F = false
27
+ SCFNAME = File.basename(__FILE__)
28
+ FO = FileOverwrite
29
+
30
+ def setup
31
+ @tmpioin = Tempfile.new
32
+ @tmpioin.sync=true
33
+
34
+ @orig_path = @tmpioin.path
35
+ @orig_content = "1 line A\n2 line B\n3 line C\n"
36
+ @orig_mtime = Time.now - 86400 # A day earlier
37
+
38
+ @tmpioin.print @orig_content
39
+ reset_mtime
40
+ end
41
+
42
+ def teardown
43
+ @tmpioin.close
44
+ end
45
+
46
+ def reset_mtime(fname=@tmpioin.path)
47
+ FileUtils.touch fname, :mtime => @orig_mtime
48
+ end
49
+
50
+ def create_template_file
51
+ io = Tempfile.new
52
+ io.sync=true
53
+ io.print @orig_content
54
+ reset_mtime(io.path)
55
+ io
56
+ end
57
+
58
+ def test_initialize01
59
+ # Testing backup file name
60
+ f1 = FO.new(@tmpioin.path)
61
+ re = /(.*)\.\d{14}\.bak$/
62
+ assert_match(re, f1.backup)
63
+ m = re.match f1.backup
64
+ assert_equal @tmpioin.path, m[1]
65
+
66
+ s = 'tekito'
67
+ f1.backup = s
68
+ assert_equal s, f1.backup
69
+
70
+ assert_equal @orig_content, f1.dump
71
+
72
+ # Testing backup file name
73
+ f2 = FO.new(@tmpioin.path, suffix: '~')
74
+ re = /(.*)~$/
75
+ assert_match(re, f2.backup)
76
+ m = re.match f2.backup
77
+ assert_equal @tmpioin.path, m[1]
78
+ end
79
+
80
+ def test_backup
81
+ file1 = create_template_file
82
+ fpath = file1.path
83
+ suffix1 = '.BAK'
84
+ f1 = FO.new(fpath, suffix: suffix1)
85
+ assert_equal fpath+suffix1, f1.backup
86
+ assert_equal fpath+'other', f1.backup('other') # Just to see
87
+ assert_equal fpath+suffix1, f1.backup # No change has been made after just "seeing"
88
+ f1.backup = fpath+'other'
89
+ assert_equal fpath+'other', f1.backup # Changed.
90
+ f1.backup = fpath+suffix1
91
+ assert_equal fpath+suffix1, f1.backup # Reverted back
92
+ end
93
+
94
+ def test_open # modify
95
+ file1 = create_template_file
96
+ fpath = file1.path
97
+ f1 = FO.new(fpath)
98
+ s = 'tekito'
99
+
100
+ # Testing open
101
+ f1.open{|ior, iow|
102
+ # Line 1
103
+ line = ior.gets
104
+ assert_equal 1, line.count("\n")
105
+ assert_equal '1', line[0,1]
106
+ iow.print line
107
+
108
+ # Line 2
109
+ line = ior.gets
110
+ assert_equal 1, line.count("\n")
111
+ assert_equal '2', line[0,1]
112
+ iow.print line
113
+
114
+ # Line 3
115
+ line = ior.gets
116
+ assert_equal 1, line.count("\n")
117
+ assert_equal '3', line[0,1]
118
+ iow.print line
119
+ iow.print s
120
+ }
121
+ assert_equal @orig_content+s, f1.dump
122
+
123
+ # Testing open
124
+ bkupfile = f1.temporary_filename # instance_eval{@iotmp.path}
125
+ assert File.exist?(bkupfile)
126
+ f1.reset
127
+ assert f1.reset?
128
+ refute File.exist?(bkupfile)
129
+ assert_nil f1.temporary_filename
130
+
131
+ # .modify
132
+ f1.modify{|ior, iow|
133
+ iow.print s+s
134
+ }
135
+ assert_equal s+s, f1.dump
136
+
137
+ # Testing warning issued if changing to String-manipulation mode
138
+ bkupfile = f1.temporary_filename # instance_eval{@iotmp.path}
139
+ assert File.exist?(bkupfile)
140
+
141
+ assert_output('', /WARNING\b.+\breread/i){ f1.read{|i| s} }
142
+ refute File.exist?(bkupfile)
143
+ assert_nil f1.temporary_filename
144
+ end
145
+
146
+ def test_read
147
+ file1 = create_template_file
148
+ fpath = file1.path
149
+ f1 = FO.new(fpath)
150
+
151
+ s = 'tekito'
152
+ f1.read{|i| s}
153
+ assert_equal s, f1.dump
154
+
155
+ # Test of replace_with()
156
+ f1.replace_with(s+s)
157
+ assert_equal s+s, f1.dump
158
+ assert_equal String, f1.state
159
+
160
+ # Test of reset()
161
+ f1.reset
162
+ assert_equal @orig_content, f1.dump
163
+ assert f1.fresh?
164
+ assert f1.reset?
165
+ assert_nil f1.state
166
+
167
+ assert_output('', / not opened,/){ f1.run!(verbose: true) }
168
+ assert_equal @orig_content, File.read(fpath)
169
+ assert_in_delta(@orig_mtime, File.mtime(fpath), 1) # 1 sec allowance
170
+ end
171
+
172
+ def test_open_run_noop # modify
173
+ file1 = create_template_file
174
+ fpath = file1.path
175
+ suffix1 = '.BAK'
176
+ f1 = FO.new(fpath, suffix: suffix1)
177
+ assert_equal fpath+suffix1, f1.backup
178
+
179
+ s = 'tekito'
180
+ f1.open{|ior, iow|
181
+ iow.print ior.read + s
182
+ }
183
+ assert_equal IO, f1.state
184
+
185
+ assert_output('', /Dryrun.+File.+Size.+Backup/){ f1.run!(noop: true, verbose: true) }
186
+ # => "[Dryrun]File /var/XXX updated (Size: 18 => 24 bytes, Backup: /var/XXX.BAK)"
187
+
188
+ sizes = f1.sizes
189
+ assert_equal s.size, sizes[:new]-sizes[:old]
190
+ # assert_nil f1.sizes
191
+ assert_equal fpath+suffix1, f1.backup
192
+ assert_in_delta(@orig_mtime, File.mtime(fpath), 1) # b/c noop
193
+
194
+ assert f1.completed?
195
+ assert f1.frozen?
196
+ assert_equal true, f1.state
197
+ assert_raises(FileOverwriteError){ f1.run! }
198
+ assert_equal fpath+suffix1, f1.backup
199
+ assert_raises(FrozenError){ f1.backup = fpath+'other' } # Can't be modified any more.
200
+ assert_raises(FrozenError){ f1.reset }
201
+ end
202
+
203
+
204
+ def test_open_run_real # modify
205
+ file1 = create_template_file
206
+ fpath = file1.path
207
+ fsize = File.size fpath
208
+ suffix1 = '.BAK'
209
+ f1 = FO.new(fpath, suffix: suffix1)
210
+ assert_equal fpath+suffix1, f1.backup
211
+
212
+ s = 'tekito'
213
+ f1.open{|ior, iow|
214
+ iow.print ior.read + s
215
+ }
216
+ assert_equal IO, f1.state
217
+
218
+ assert_output('', /File.+Size.+Backup/){ f1.run!(noop: false, verbose: true) }
219
+ # => "File /var/XXX updated (Size: 18 => 24 bytes, Backup: /var/XXX.BAK)"
220
+
221
+ # Sizes of the files.
222
+ sizes = f1.sizes
223
+ assert_equal s.size, sizes[:new]-sizes[:old]
224
+ assert_equal fsize, sizes[:old]
225
+ assert_equal File.size(f1.backup), sizes[:old]
226
+ assert_equal File.size(fpath), sizes[:new]
227
+
228
+ # Timestamps of the files.
229
+ assert_equal fpath+suffix1, f1.backup
230
+ refute_in_delta(@orig_mtime, File.mtime(fpath), 1)
231
+ assert_operator(@orig_mtime, '<', File.mtime(fpath))
232
+
233
+ assert f1.completed?
234
+ assert f1.frozen?
235
+ assert_equal true, f1.state
236
+ assert_raises(FileOverwriteError){ f1.run! }
237
+ assert_equal fpath+suffix1, f1.backup
238
+ assert_raises(FrozenError){ f1.backup = fpath+'other' } # Can't be modified any more.
239
+ assert_raises(FrozenError){ f1.reset }
240
+ end
241
+
242
+ def test_read_run_real
243
+ file1 = create_template_file
244
+ fpath = file1.path
245
+ fsize = File.size fpath
246
+ f1 = FO.new(fpath, suffix: nil)
247
+ fbkup = f1.backup
248
+ assert_nil fbkup
249
+
250
+ s = 'tekito'
251
+ f1.read{|i| i + s}
252
+ assert_equal String, f1.state
253
+
254
+ assert_output('', /File.+Size:[\d =>]+bytes?.$/){ f1.run!(verbose: true) }
255
+ # => "File /var/XXX updated (Size: 18 => 24 bytes)" (No word: "Backup")
256
+ assert_nil f1.backup
257
+
258
+ assert_equal @orig_content+s, File.read(fpath)
259
+
260
+ # Sizes of the files.
261
+ sizes = f1.sizes
262
+ assert_equal s.size, sizes[:new]-sizes[:old]
263
+ assert_equal fsize, sizes[:old]
264
+ assert_equal @orig_content.size, sizes[:old]
265
+ assert_equal File.size(fpath), sizes[:new]
266
+
267
+ # Timestamps of the files.
268
+ refute_in_delta(@orig_mtime, File.mtime(fpath), 1)
269
+ assert_operator(@orig_mtime, '<', File.mtime(fpath))
270
+ end
271
+
272
+ def test_read_run_bang
273
+ file1 = create_template_file
274
+ fpath = file1.path
275
+ f1 = FO.new(fpath, suffix: nil, verbose: true)
276
+ s = 'tekito'
277
+
278
+ # Test of silent as well
279
+ assert_output('', ''){ f1.read!(verbose: nil){|i| i + s} }
280
+ assert_equal @orig_content+s, File.read(fpath)
281
+ sizes = f1.sizes
282
+ assert_equal s.size, sizes[:new]-sizes[:old]
283
+
284
+ # Test of setsize option
285
+ file1 = create_template_file
286
+ fpath = file1.path
287
+ f1 = FO.new(fpath, suffix: nil, verbose: nil)
288
+ assert_output('', ''){ f1.read!(setsize: false){|i| i + s} }
289
+ assert_equal @orig_content+s, File.read(fpath)
290
+ assert_nil f1.sizes
291
+ end
292
+
293
+ def test_sub
294
+ file1 = create_template_file
295
+ fpath = file1.path
296
+ f1 = FO.new(fpath, suffix: nil, verbose: true)
297
+
298
+ f1.sub(/line/, 'xyz')
299
+ assert_match(/xyz/, f1.dump.split(/\n/)[0])
300
+ refute_match(/xyz/, f1.dump.split(/\n/)[1])
301
+ f1.reset
302
+ refute_match(/xyz/, f1.dump.split(/\n/)[0])
303
+
304
+ assert_output('', ''){ f1.gsub(/line/, 'xyz') }
305
+ assert_match(/xyz/, f1.dump.split(/\n/)[0])
306
+ assert_match(/xyz/, f1.dump.split(/\n/)[1])
307
+ f1.reset
308
+ refute_match(/xyz/, f1.dump.split(/\n/)[0])
309
+
310
+ f1.sub(/(li)(n)/, '\1' + 'k')
311
+ assert_match(/like/, f1.dump.split(/\n/)[0])
312
+ refute_match(/like/, f1.dump.split(/\n/)[1])
313
+ f1.reset
314
+ refute_match(/like/, f1.dump.split(/\n/)[0]) # not "like" but "line"
315
+
316
+ # Failed-match case
317
+ f1.sub(/naiyo(li)(n)/, '\1' + 'k')
318
+ assert_match(/line/, f1.dump.split(/\n/)[0])
319
+ assert_equal @orig_content, f1.instance_eval{@outstr}
320
+ f1.reset
321
+
322
+ ### The following does NOT work!
323
+ # f1.sub(/(li)(ne)/){printf "and=(%s)(%s)", $&, $1; 'LIne'} # $1.upcase + $2
324
+
325
+ ### MatchData extension in this method!
326
+ f1.sub(/(li)(n)/){|_,m| m[1].upcase+m[2]} # $1.upcase + $2
327
+
328
+ assert_match(/LIne/, f1.dump.split(/\n/)[0], "DEBUG: "+f1.dump)
329
+ refute_match(/LIne/, f1.dump.split(/\n/)[1])
330
+ f1.sub(/I/){nil} # nil.to_s (as in String#sub)
331
+ assert_match(/\bLne\b/, f1.dump.split(/\n/)[0])
332
+ f1.sub(/L/){3} # 3.to_s (as in String#sub)
333
+ assert_match(/\b3ne\b/, f1.dump.split(/\n/)[0])
334
+ f1.reset
335
+ refute_match(/LI?ne/, f1.dump.split(/\n/)[0])
336
+ assert_nil f1.instance_eval{@outstr}
337
+
338
+ # Failed-match case
339
+ f1.sub(/naiyo(li)(n)/){|_,m| m[1].upcase+m[2]} # $1.upcase + $2
340
+ assert_match(/line/, f1.dump.split(/\n/)[0], "DEBUG: "+f1.dump)
341
+ f1.reset
342
+ end
343
+
344
+
345
+ def test_gsub
346
+ file1 = create_template_file
347
+ fpath = file1.path
348
+ f1 = FO.new(fpath, suffix: nil, verbose: true)
349
+
350
+ assert_output('', ''){ f1.gsub(/line/, 'xyz') }
351
+ assert_match(/xyz/, f1.dump.split(/\n/)[0])
352
+ assert_match(/xyz/, f1.dump.split(/\n/)[1])
353
+ assert_match(/xyz/, f1.dump.split(/\n/)[2])
354
+ f1.reset
355
+ refute_match(/xyz/, f1.dump.split(/\n/)[0])
356
+
357
+ assert_output('', ''){ f1.gsub(/line/, 'xyz', max: 1) }
358
+ assert_match(/xyz/, f1.dump.split(/\n/)[0])
359
+ refute_match(/xyz/, f1.dump.split(/\n/)[1])
360
+ f1.reset
361
+ refute_match(/xyz/, f1.dump.split(/\n/)[0])
362
+
363
+ ## max option is ignored for the case without a block
364
+ assert_output('', /WARNING\b.+\bmax/i){ f1.gsub(/line/, 'xyz', max: 2) }
365
+ f1.reset
366
+
367
+ assert_output(nil){ f1.gsub(/line/){ 'xyz' } }
368
+ assert_match(/xyz/, f1.dump.split(/\n/)[0])
369
+ assert_match(/xyz/, f1.dump.split(/\n/)[1])
370
+ assert_match(/xyz/, f1.dump.split(/\n/)[2])
371
+ f1.reset
372
+ refute_match(/xyz/, f1.dump.split(/\n/)[0])
373
+
374
+ assert_output('', ''){ f1.gsub(/line/, max: 2){ 'xyz' } }
375
+ assert_match(/xyz/, f1.dump.split(/\n/)[0])
376
+ assert_match(/xyz/, f1.dump.split(/\n/)[1])
377
+ refute_match(/xyz/, f1.dump.split(/\n/)[2])
378
+ f1.reset
379
+ refute_match(/xyz/, f1.dump.split(/\n/)[0])
380
+ end
381
+
382
+
383
+ def test_tr
384
+ file1 = create_template_file
385
+ fpath = file1.path
386
+ f1 = FO.new(fpath, suffix: nil, verbose: true)
387
+
388
+ refute f1.empty?
389
+ f1.tr('n', 'e').tr('i', 's')
390
+ f1.tr('l', '') # => "1 see A\n2 see B\n"
391
+ assert_equal String, f1.state
392
+ assert_match(/\bsee\b/, f1.dump.split(/\n/)[0])
393
+ assert_match(/\bsee\b/, f1.dump.split(/\n/)[1])
394
+ f1.tr('@', '') # => "1 see A\n2 see B\n"
395
+
396
+ assert_match(/\bsee\b/, f1.dump.split(/\n/)[0]) # No change
397
+
398
+ ## tr_s ###
399
+ f1.tr_s('e', 'i') # => "1 si A\n2 si B\n"
400
+ assert_equal String, f1.state
401
+ assert_match(/\bsi\b/, f1.dump.split(/\n/)[0])
402
+ assert_match(/\bsi\b/, f1.dump.split(/\n/)[1])
403
+
404
+ ## tr_s! ###
405
+ f1.reset
406
+ assert_equal @orig_content, f1.dump
407
+
408
+ f1.tr('l', 'L')
409
+ assert_output(nil){ f1.tr_s!('@', '@') } # No change, but save. STDOUT/ERR suppressed.
410
+ assert_equal @orig_content.tr('l', 'L'), File.read(fpath)
411
+ sizes = f1.sizes
412
+ assert_equal 0, sizes[:new]-sizes[:old]
413
+ end
414
+
415
+ def test_each_line
416
+ file1 = create_template_file
417
+ fpath = file1.path
418
+ f1 = FO.new(fpath, suffix: nil, verbose: true)
419
+
420
+ f1.each_line{ |i|
421
+ i.sub(/li/, 'sX')
422
+ }.each_line{ |i|
423
+ i.sub(/sX/, 'si')
424
+ }
425
+ assert_match(/\bsine\b/, f1.dump.split(/\n/)[0])
426
+ assert_match(/\bsine\b/, f1.dump.split(/\n/)[1])
427
+
428
+ assert_raises(ArgumentError){ f1.each_line }
429
+
430
+ ## each_line! ###
431
+ f1.reset
432
+ assert_equal @orig_content, f1.dump
433
+ assert_output(nil){ f1.each_line!{ |i| i.tr('l', 'L') } } # No change in size, but save. STDOUT/ERR suppressed.
434
+ assert_equal @orig_content.tr('l', 'L'), File.read(fpath)
435
+ sizes = f1.sizes
436
+ assert_equal 0, sizes[:new]-sizes[:old]
437
+ end
438
+
439
+
440
+ def test_readlines
441
+ file1 = create_template_file
442
+ fpath = file1.path
443
+ s = 'tekito'
444
+ # print 'DEBUG:$VERBOSE=';p $VERBOSE # => true (in Testing mode?)
445
+
446
+ f1 = FO.readlines(fpath, verbose: false){ |ea|
447
+ assert_equal @orig_content, ea.join('')
448
+ orig_chomp = @orig_content.chomp
449
+ assert_equal orig_chomp.split(/\n/).size, ea.size
450
+ assert_equal orig_chomp.split(/\n/)[0], ea[0].chop
451
+ assert_equal orig_chomp.split(/\n/)[-1], ea[-1].chop
452
+ [s]
453
+ }
454
+ assert_equal s, f1.dump
455
+ assert_nil f1.temporary_filename # No temporary file is written in Array-mode
456
+ refute f1.verbose, sprintf("verbose=%s", f1.verbose.inspect)
457
+
458
+ ## run!
459
+ assert_output('', ''){f1.run}
460
+ sizes = f1.sizes
461
+ assert_equal @orig_content.size, sizes[:old]
462
+ assert_equal s.size, sizes[:new]
463
+ end
464
+
465
+ end # class TestUnitFileOverwrite < MiniTest::Test
466
+
467
+ #end # if $0 == __FILE__
468
+
469
+
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: file_overwrite
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Masa Sakano
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-09-19 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: This class provides a Ruby-oriented scheme to safely overwrite an existing
14
+ file, leaving a backup file unless specified otherwise. It writes a temporary file
15
+ first, which is renamed to the original file in one action. It accepts a block
16
+ like some IO class-methods (e.g., each_line) and chaining like String methods (e.g.,
17
+ sub and gsub).
18
+ email:
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files:
22
+ - README.en.rdoc
23
+ files:
24
+ - ".gitignore"
25
+ - ChangeLog
26
+ - Makefile
27
+ - README.en.rdoc
28
+ - Rakefile
29
+ - file_overwrite.gemspec
30
+ - lib/file_overwrite/file_overwrite.rb
31
+ - lib/file_overwrite/file_overwrite_error.rb
32
+ - test/test_file_overwrite.rb
33
+ homepage: https://www.wisebabel.com
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ yard.run: yri
38
+ post_install_message:
39
+ rdoc_options:
40
+ - "--charset=UTF-8"
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubyforge_project:
55
+ rubygems_version: 2.7.3
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: Class to overwrite an existing file safely
59
+ test_files:
60
+ - test/test_file_overwrite.rb