treebis 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/treebis.rb ADDED
@@ -0,0 +1,1516 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'open3'
4
+ require 'pathname'
5
+ require 'shellwords'
6
+ require 'tempfile'
7
+
8
+ module Treebis
9
+
10
+ @task_set = nil
11
+ class << self
12
+ def dir_task path
13
+ taskfile = path + '/treebis-task.rb'
14
+ require taskfile
15
+ name = File.basename(path).to_sym
16
+ task = @task_set[name]
17
+ task.from path
18
+ task
19
+ end
20
+ def tasks
21
+ @task_set ||= TaskSet.new
22
+ end
23
+ def unindent content
24
+ /\A(\s*)/ =~ content
25
+ content.gsub(/^#{Regexp.escape($1)}/,'')
26
+ end
27
+ end
28
+
29
+ module Antecedent
30
+ def init_path_antecedent
31
+ @antecedent = {}
32
+ end
33
+ def path_antecedent domain, string
34
+ if @antecedent[domain] && head =common_head(@antecedent[domain], string)
35
+ @antecedent[domain] = [@antecedent[domain], string].max_by(&:length)
36
+ path_antecedent_truncate head, string
37
+ else
38
+ @antecedent[domain] = string
39
+ string
40
+ end
41
+ end
42
+ private
43
+ def path_antecedent_truncate common_head, string
44
+ tail = string[common_head.length..-1]
45
+ common_head =~ %r<(.{0,3}/?[^/]+/?)\Z>
46
+ head = $1
47
+ "...#{head}#{tail}"
48
+ end
49
+ def common_head str1, str2
50
+ for i in 0..( [str1.length, str2.length].min - 1 )
51
+ break unless str1[i] == str2[i]
52
+ end
53
+ ret = str1[0..i-1]
54
+ ret
55
+ end
56
+ end
57
+ # like Open3 but stay in the ruby runtime
58
+ module Capture3
59
+ # @return [out_string, err_string, result]
60
+ def capture3 &block
61
+ prev_out, prev_err, result = $stdout, $stderr, nil
62
+ out_str, err_str = nil, nil
63
+ $stderr = StringIO.new
64
+ $stdout = StdoutStringIoHack.new
65
+ begin
66
+ result = block.call
67
+ ensure
68
+ $stderr.rewind
69
+ out_str = $stdout.to_str
70
+ err_str = $stderr.read
71
+ $stdout = prev_out
72
+ $stderr = prev_err
73
+ end
74
+ [out_str, err_str, result]
75
+ end
76
+ class StdoutStringIoHack < File
77
+ private(*ancestors[1..2].map(&:public_instance_methods).flatten)
78
+ these = %w(puts write <<)
79
+ public(*these)
80
+ def initialize
81
+ super('/dev/null','w+')
82
+ @buffer = StringIO.new
83
+ end
84
+ these.each do |this|
85
+ alias_method "old_#{this}", this # avoid silly warnings
86
+ define_method(this){|*a| @buffer.send(this, *a) }
87
+ end
88
+ def to_str
89
+ @buffer.rewind
90
+ @buffer.read
91
+ end
92
+ end
93
+ end
94
+ module Colorize
95
+ Codes = {:bright=>'1', :red=>'31', :green=>'32', :yellow=>'33',
96
+ :blue=>'34',:magenta=>'35',:bold=>'1',:blink=>'5'}
97
+ def colorize str, *codenames
98
+ return str if codenames == [nil] || codenames.empty?
99
+ codes = nil
100
+ if codenames.first == :background
101
+ fail("not yet") unless codenames.size == 2
102
+ codes = ["4#{Codes[codenames.last][1..1]}"]
103
+ # this isn't really excusable in any way
104
+ else
105
+ codes = codenames.map{|x| Codes[x]}
106
+ end
107
+ "\e["+codes.join(';')+"m#{str}\e[0m"
108
+ end
109
+ module_function :colorize
110
+ end
111
+ module Config
112
+ extend self
113
+ @color = true
114
+ def color?; @color end
115
+ def no_color!; @color = false end
116
+ def color!; @color = true end
117
+
118
+ def default_out_stream
119
+ proc{ $stderr } # for capture_3 to work this has
120
+ # to act like a referece to this global variable
121
+ end
122
+
123
+ @default_prefix = ' '*6 # sorta like nanoc
124
+ attr_accessor :default_prefix
125
+
126
+ def new_default_file_utils_proxy
127
+ FileUtilsProxy.new do |fu|
128
+ fu.prefix = default_prefix
129
+ fu.ui = default_out_stream
130
+ end
131
+ end
132
+ end
133
+
134
+ module DirAsHash
135
+ #
136
+ # Return an entire filsystem node as a hash whose files are represented
137
+ # as strings holding the contents of the file and whose folders
138
+ # are other such hashes. The hash element keys are the entry strings.
139
+ # Careful! the only real use for this so far is in testing.
140
+ #
141
+ def dir_as_hash path, opts={}
142
+ blacklist = Blacklist.get(opts[:skip])
143
+ list1 = Dir.new(path).each.map.reject{ |x|
144
+ 0==x.index('.') || blacklist.include?(x)
145
+ }
146
+ list2 = list1.map{ |entry|
147
+ p = path + '/' + entry
148
+ [ entry, File.directory?(p) ?
149
+ dir_as_hash(p, :skip=>blacklist.submatcher(entry)) : File.read(p) ]
150
+ }
151
+ Hash[ list2 ]
152
+ end
153
+ module_function :dir_as_hash
154
+
155
+ def hash_to_dir hash, path, file_utils=FileUtils
156
+ fail("must not exist: #{path}") if File.exist? path
157
+ file_utils.mkdir_p(path,:verbose=>true)
158
+ hash.each do |k,v|
159
+ path2 = File.join(path, k)
160
+ case v
161
+ when String
162
+ File.open(path2, 'w'){|fh| fh.write(v)}
163
+ when Hash
164
+ hash_to_dir(v, path2, file_utils)
165
+ else
166
+ fail("bad type for dir hash: #{v.inspect}")
167
+ end
168
+ end
169
+ true
170
+ end
171
+ module_function :hash_to_dir
172
+
173
+ class Blacklist
174
+ #
175
+ # ruby implementation of fileglob-like patterns for data structures
176
+ # this would be faster and uglier with string matchers as hash keys
177
+ # this might move to Tardo lib
178
+ #
179
+ module MatcherFactory
180
+ def create_matcher mixed
181
+ case mixed
182
+ when Matcher; mixed # keep this first for primitive subclasses
183
+ when NilClass; EmptyMatcher
184
+ when String
185
+ %r{\A([^/]+)(?:/(.+)|)\Z} =~ mixed or
186
+ fail("path: #{mixed.inspect}")
187
+ head, tail = $1, $2
188
+ if head.index('*')
189
+ GlobNodeMatcher.get(head, tail)
190
+ else
191
+ StringMatcher.new(head, tail)
192
+ end
193
+ when Array; Blacklist.new(mixed)
194
+ else
195
+ fail("can't build matcher from #{mixed.inspect}")
196
+ end
197
+ end
198
+ end
199
+ module Matcher; end
200
+ include Matcher, MatcherFactory
201
+ class << self
202
+ include MatcherFactory
203
+ alias_method :get, :create_matcher
204
+ end
205
+ private
206
+ def initialize lizt
207
+ @matchers = lizt.map do |glob|
208
+ create_matcher(glob)
209
+ end
210
+ end
211
+ public
212
+ def include? str
213
+ @matchers.detect{ |x| x.include?(str) }
214
+ end
215
+ def submatcher str
216
+ these = []
217
+ @matchers.each do |m|
218
+ this = m.submatcher(str)
219
+ these.push(this) if this
220
+ end
221
+ ret =
222
+ if these.empty?
223
+ nil
224
+ else
225
+ Blacklist.new(these)
226
+ end
227
+ ret
228
+ end
229
+ private
230
+ class EmptyMatcherClass
231
+ include Matcher
232
+ def initialize; end
233
+ def submatcher m
234
+ self
235
+ end
236
+ def include? *a
237
+ false
238
+ end
239
+ end
240
+ EmptyMatcher = EmptyMatcherClass.new
241
+ class MatcherWithTail
242
+ include MatcherFactory, Matcher
243
+ def include? str
244
+ if @tail
245
+ false
246
+ else
247
+ head_include?(str)
248
+ end
249
+ end
250
+ def tail= str
251
+ if str.nil?
252
+ @tail = nil
253
+ else
254
+ @tail = create_matcher(str)
255
+ end
256
+ end
257
+ def submatcher(sub)
258
+ if head_include?(sub)
259
+ @tail
260
+ else
261
+ nil
262
+ end
263
+ end
264
+ end
265
+ class GlobNodeMatcher < MatcherWithTail
266
+ # **/ --> (.*?\/)+ ; * --> .*? ; ? --> . ;
267
+ # "**/*.rb" --> /^(.*?\/)+.*?\.rb$/ -thx heftig
268
+ class << self
269
+ include MatcherFactory
270
+ def get globtoken, tail
271
+ if /(?:\*\*|\/)/ =~ globtoken
272
+ GlobNodeMatcherDeep.new(globtoken, tail)
273
+ else
274
+ new globtoken, tail
275
+ end
276
+ end
277
+ end
278
+ def initialize globtoken, tail
279
+ /(?:\*\*|\/)/ =~ globtoken and fail("use factory")
280
+ /\*/ =~ globtoken or fail("no")
281
+ regexp_str = globtoken.split('*').map do |x|
282
+ Regexp.escape(x)
283
+ end.join('.*?')
284
+ @re = Regexp.new("\\A#{regexp_str}\\Z")
285
+ self.tail = tail
286
+ end
287
+ def head_include? str
288
+ @re =~ str
289
+ end
290
+ end
291
+ class GlobNodeMatcherDeep < GlobNodeMatcher
292
+ def initialize globtoken, tail
293
+ /\*\*/ =~ globtoken or fail("not deep: #{globtoken.inspect}")
294
+ tail or fail("for now deep globs must look like \"**/*.bar\"")
295
+ self.tail = tail
296
+ end
297
+ undef_method :head_include? # just to be safe, we don't want this
298
+ def include? str
299
+ @tail.include? str
300
+ end
301
+ def submatcher str
302
+ self # you just keep travelling down the tree when you're '**'
303
+ end
304
+ end
305
+ class StringMatcher < MatcherWithTail
306
+ def initialize str, tail=nil
307
+ @head = str
308
+ self.tail = tail
309
+ end
310
+ def head_include? str
311
+ @head == str
312
+ end
313
+ end
314
+ end
315
+ end
316
+ class Fail < RuntimeError; end
317
+ class FileUtilsAsClass
318
+ # make it so we can effectively subclass FileUtils and override methods
319
+ # and call up to the originals
320
+ include FileUtils
321
+ public :cp, :mkdir_p, :mv, :rm, :remove_entry_secure
322
+ end
323
+ module Stylize; end
324
+ class FileUtilsProxy < FileUtilsAsClass
325
+ # We like the idea of doing FileUtils.foo(:verbose=>true) and outputting
326
+ # whatever the response it to screen, but sometimes we want to format its
327
+ # response a little more when possible.
328
+ #
329
+ include Antecedent, Capture3, Colorize, Stylize
330
+ def initialize &block
331
+ @color = nil
332
+ @prefix = ''
333
+ @pretty = false
334
+ init_path_antecedent
335
+ @ui = Config.default_out_stream
336
+ @ui_stack = nil
337
+ yield(self) if block_given?
338
+ end
339
+ def color? &block
340
+ if block_given?
341
+ @color = block
342
+ else
343
+ @color.call
344
+ end
345
+ end
346
+ def cp *args
347
+ opts = args.last.kind_of?(Hash) ? args.last : {}
348
+ ret, out, err = nil, "", nil
349
+ keep = {:pretty_name_target => opts.delete(:pretty_name_target) }
350
+ if ! @pretty
351
+ ret = super(*args)
352
+ else
353
+ out, err, ret = capture3{ super(*args) }
354
+ opts.merge!(keep)
355
+ pretty_puts_cp out, err, ret, *args
356
+ end
357
+ ret
358
+ end
359
+ attr_reader :prefix
360
+ def prefix= str
361
+ @pretty = true
362
+ @prefix = str
363
+ end
364
+ def pretty!
365
+ @pretty = true
366
+ end
367
+ def remove_entry_secure path, force=false, opts={}
368
+ opts = {:verbose=>true}.merge(opts)
369
+ # parent call doesn't write to stdout or stderr, returns nil
370
+ did = nil
371
+ if File.exist?(path)
372
+ super(path, force)
373
+ did = true
374
+ err = "rm -rf #{path}"
375
+ colors = [:bright,:green]
376
+ else
377
+ did = false
378
+ err = "rm -rf (didn't exist:) #{path}"
379
+ colors = [:bright, :red]
380
+ end
381
+ if opts[:verbose]
382
+ if @pretty
383
+ err.sub!(/\Arm -rf/){colorize('rm -rf',*colors)}
384
+ err = "#{prefix}#{err}"
385
+ end
386
+ ui.puts err
387
+ end
388
+ did
389
+ end
390
+ def rm *a
391
+ if @pretty
392
+ b = capture3{ super(*a) }
393
+ pretty_puts_rm(*(b+a))
394
+ else
395
+ ret = super(*a)
396
+ end
397
+ ret
398
+ end
399
+ def not_pretty!
400
+ @pretty = false
401
+ end
402
+ def mkdir_p *args
403
+ ret, out, err = nil, "", nil
404
+ if File.exist?(args.first)
405
+ err = "treebis: mkdir_p: exists: #{args.first}"
406
+ elsif @pretty
407
+ out, err, ret = capture3{ super(*args) }
408
+ else
409
+ ret = super
410
+ end
411
+ pretty_puts_mkdir_p out, err, ret, *args
412
+ ret
413
+ end
414
+ # keep this public because runner context refers to it
415
+ attr_writer :ui
416
+ def ui
417
+ @ui.kind_of?(Proc) ? @ui.call : @ui # see Config
418
+ end
419
+ # not sure about this, it breaks the otherwise faithful 'api'
420
+ # we don't like it because of that, we do like it better than
421
+ # capture3 because we don't need ugly hacks or tossing around globals
422
+ def ui_push
423
+ @ui_stack ||= []
424
+ @ui_stack.push @ui
425
+ @ui = StringIO.new
426
+ end
427
+ def ui_pop
428
+ it = @ui
429
+ @ui = @ui_stack.pop
430
+ resp = it.kind_of?(StringIO) ? (it.rewind && it.read) : it
431
+ resp
432
+ end
433
+ private
434
+ def pretty_puts_cp out, err, ret, *args
435
+ fail("unexpected out: #{out.inspect} or ret: #{ret.inspect}") unless
436
+ ret.nil? && out == ''
437
+ opts = args.last.kind_of?(Hash) ? args.last : {}
438
+ p1, p2 = pretty_puts_cp_paths(args[0], args[1], opts)
439
+ # Regexp.escape(args[0..1].map{|x|Shellwords.escape(x)}.join(' '))
440
+ matched =
441
+ /\Acp #{Regexp.escape(args[0])} #{Regexp.escape(args[1])}(.*)\Z/ =~ err
442
+ if err.empty?
443
+ err = "no response? cp #{p1} #{p2}"
444
+ elsif matched
445
+ err = "cp #{p1} #{p2}#{$1}"
446
+ end
447
+ err.sub!(/\A(?:no response\? )?cp\b/){colorize('cp',:bright,:green)}
448
+ ui.puts("%s%s" % [prefix, err])
449
+ end
450
+ # use a variety of advanced cutting edge technology to determine
451
+ # a shortest way to express unambiguesque-ly paths
452
+ def pretty_puts_cp_paths path1, path2, opts
453
+ path2 = if opts[:pretty_name_target]
454
+ opts[:pretty_name_target]
455
+ else
456
+ s1 = Pathname.new(path2).relative_path_from(Pathname.new(path1)).to_s
457
+ s2 = path_antecedent(:cp_tgt, path2)
458
+ s3 = path2
459
+ shortest, idx = [s1, s2, s3].each_with_index.min_by{|v, i|v.length}
460
+ shortest
461
+ end
462
+ path1 = path_antecedent(:cp_src, path1)
463
+ [path1, path2]
464
+ end
465
+ def pretty_puts_mkdir_p out, err, ret, *args
466
+ fail("expecing mkdir_p never to write to stdout") unless out == ""
467
+ return unless args.last.kind_of?(Hash) && args.last[:verbose]
468
+ if @pretty
469
+ err2 = err.empty? ? "mkdir -p #{ret}" : err # sometimes this. weird
470
+ err3 = err2.sub(/\A(mkdir -p)/){ colorize($1, :bright, :green)}
471
+ ui.puts sprintf("#{prefix}#{err3}")
472
+ else
473
+ ui.puts(err) unless err == "" # emulate it
474
+ end
475
+ end
476
+ def pretty_puts_rm out, err, ret, *a
477
+ if out=='' && err==''
478
+ err = "rm #{ret.join(' ')}"
479
+ color = :green
480
+ # @todo what's happening here? (change color to red)
481
+ else
482
+ color = :green
483
+ end
484
+ err = err.sub(/\Arm\b/){colorize('rm', :bright, color)}
485
+ ui.puts sprintf("#{prefix}#{err}")
486
+ end
487
+ end
488
+ module PathString
489
+ def no_leading_dot_slash str
490
+ str.sub(/\A\.\//,'')
491
+ end
492
+ end
493
+ module Shellopts
494
+ def shellopts hash
495
+ hash.map.flatten.reject{|x| x.empty? || x.nil? }
496
+ end
497
+ end
498
+ module Sopen2
499
+ @ui = nil
500
+ module_function
501
+ def sopen2 *a
502
+ Open3.popen3(*a) do |ins,out,err|
503
+ return [out.read, err.read]
504
+ end
505
+ end
506
+ def sopen2assert *a
507
+ out, err = sopen2(*a)
508
+ if err.length > 0
509
+ stderr.puts err
510
+ fail(err.split("\n").first+"...")
511
+ else
512
+ out
513
+ end
514
+ end
515
+ def stderr
516
+ @stderr ||= ((@ui && @ui.respond_to?(:err)) ? @ui.err : $stderr)
517
+ end
518
+ end
519
+ module Stylize
520
+ # includer must implement color? and prefix() and ui()
521
+
522
+ include Colorize
523
+
524
+ ReasonStyles = { :identical => :skip, :"won't overwrite" => :skip,
525
+ :changed => :did, :wrote => :did, :exists=>:skip,
526
+ :patched => :changed2, :notice => :changed2,
527
+ :exec => :changed2}
528
+ StyleCodes = { :skip => [:bold, :red], :did => [:bold, :green] ,
529
+ :changed2 => [:bold, :yellow] }
530
+
531
+ #
532
+ # @return nil if not found. feel free to override.
533
+ #
534
+ def get_style foo
535
+ StyleCodes[ReasonStyles[foo]]
536
+ end
537
+
538
+ #
539
+ # write to ui.puts() whatever you want with the notice style. if color?,
540
+ # colorize head and separate it with a space and print tail without color.
541
+ # if not color?, the above but in "b&w"
542
+ #
543
+ def notice head, tail
544
+ if color? # && /\A(\s*\S+)(.*)\Z/ =~ msg
545
+ msg = colorize(head, * get_style(:notice))+' '+tail
546
+ else
547
+ msg = "#{head} #{tail}"
548
+ end
549
+ ui.puts "#{prefix}#{msg}"
550
+ end
551
+ end
552
+ class TaskSet
553
+ def initialize
554
+ @tasks = {}
555
+ end
556
+ def task name, &block
557
+ fail("can't reopen task: #{name.inspect}") if @tasks.key?(name)
558
+ @tasks[name] = Task.new(name,&block)
559
+ end
560
+ def [](name)
561
+ @tasks[name]
562
+ end
563
+ end
564
+ class Task
565
+ def initialize name=nil, &block
566
+ @block = block
567
+ @file_utils = nil
568
+ @from = nil
569
+ @name = name
570
+ @ui_stack = []
571
+ end
572
+ attr_reader :erb_values
573
+ def erb_values hash
574
+ @erb_values = hash
575
+ self
576
+ end
577
+ # @api private
578
+ def erb_values_binding
579
+ @erb_values_binding ||= begin
580
+ obj = Object.new
581
+ bnd = obj.send(:binding)
582
+ sing = class << obj; self end
583
+ sing2 = class << bnd; self end
584
+ hash = @erb_values or fail("need @erb_values")
585
+ ks = hash.keys
586
+ sing2.send(:define_method, :keys){ ks }
587
+ sing2.send(:define_method, :inspect){'#<TreebisErbBinding>'}
588
+ ks.each{ |k| sing.send(:define_method, k){ hash[k] } }
589
+ bnd
590
+ end
591
+ end
592
+ def file_utils
593
+ @file_utils ||= Config.new_default_file_utils_proxy
594
+ end
595
+ def file_utils= mixed
596
+ @file_utils = mixed
597
+ end
598
+ def from path
599
+ @from = path
600
+ end
601
+ def on path
602
+ RunContext.new(self, @block, @from, path, file_utils)
603
+ end
604
+ def ui_capture &block
605
+ file_utils.ui_push
606
+ instance_eval(&block)
607
+ file_utils.ui_pop
608
+ end
609
+ class RunContext
610
+ include Colorize, PathString, Shellopts, Stylize
611
+ def initialize task, block, from_path, on_path, file_utils
612
+ @task, @block, @from_path, @on_path = task, block, from_path, on_path
613
+ @file_utils = file_utils
614
+ @noop = false # no setters yet
615
+ @opts = {}
616
+ @overwrite = false
617
+ @unindent = true # no setters yet
618
+ end
619
+ attr_accessor :file_utils, :opts
620
+ #
621
+ # if this is operating on a whole folder (if the patch filename is
622
+ # called 'diff' as opposed to 'somefile.diff'), depending on how you
623
+ # build the patch you may want the --posix flag
624
+ #
625
+ def apply diff_file, opts={}
626
+ shell_opts = shellopts(opts)
627
+ patchfile, _ = normalize_from diff_file
628
+ /\A(?:()|(.+)\.)diff\Z/ =~ diff_file or fail("patch needs .diff ext.")
629
+ basename = $1 || $2
630
+ if ! File.exist?(patchfile) && @opts[:patch_hack]
631
+ return patch_hack(diff_file)
632
+ end
633
+ target, _ = normalize_on(basename)
634
+ cmd = nil
635
+ if target == './'
636
+ shell_opts.unshift('-p0') unless shell_opts.grep(/\A-p\d+\Z/).any?
637
+ cmd = ['patch '+Shellwords.shelljoin(shell_opts)+' < '+
638
+ Shellwords.shellescape(patchfile)]
639
+ else
640
+ target_arr = (target == './') ? [] : [target]
641
+ cmd = ['patch', '-u', *shell_opts] + target_arr + [patchfile]
642
+ end
643
+ notice "patching:", cmd.join(' ')
644
+ out, err = Sopen2::sopen2(*cmd)
645
+ if err.length > 0
646
+ cmd_str = cmd.shelljoin
647
+ msg = sprintf(
648
+ "Failed to run patch command: %s\nstdout was: %sstderr was: %s",
649
+ cmd_str, out, err)
650
+ fail msg
651
+ else
652
+ pretty_puts_apply out, cmd
653
+ end
654
+ end
655
+ def erb path
656
+ require 'erb'
657
+ in_full, in_local = normalize_from path
658
+ out_full, out_local = normalize_on path
659
+ tmpl = ERB.new(File.read(in_full))
660
+ tmpl.filename = in_full # just used when errors
661
+ bnd = @task.erb_values_binding
662
+ out = tmpl.result(bnd)
663
+ write out_local, out
664
+ end
665
+ def copy path
666
+ if path.index('*')
667
+ copy_glob(path)
668
+ else
669
+ full, local = normalize_from path
670
+ copy_go full, local, path
671
+ end
672
+ end
673
+ def copy_glob path
674
+ path.index('*') or fail("not glob: #{path.inspect}")
675
+ full, local = normalize_from path
676
+ these = Dir[full]
677
+ if these.empty?
678
+ notice('copy', "no files matched #{path.inspect}")
679
+ else
680
+ opts = these.map do |this|
681
+ [this, * copy_unglob(this, local, path)]
682
+ end
683
+ opts.each {|a| copy_go(*a) }
684
+ end
685
+ end
686
+ def copy_go full, local, path
687
+ tgt = File.join(@on_path, local)
688
+ skip = false
689
+ if File.exist?(tgt) && File.read(full) == File.read(tgt)
690
+ report_action :identical, path
691
+ else
692
+ file_utils.cp(full, tgt, :verbose=>true, :noop=>@noop,
693
+ :pretty_name_target => no_leading_dot_slash(path)
694
+ )
695
+ end
696
+ end
697
+ private :copy_go
698
+ # annoying, ridiculous, fragile: make pretty reporting of glob ops
699
+ def copy_unglob full, locl, path
700
+ re = self.class.glob_to_regex(locl)
701
+ (b = full =~ re && $2 ) or fail("no: #{full.inspect}")
702
+ (d,f = locl =~re && [$1, $3]) or fail("no: #{locl.inspect}")
703
+ (h,j = path =~re && [$1, $3]) or fail("no: #{path.inspect}")
704
+ ret = ["#{d}#{b}#{f}", "#{h}#{b}#{j}"]
705
+ ret
706
+ end
707
+ private :copy_unglob
708
+ def move from, to
709
+ fr_full, fr_local = normalize_on from
710
+ to_full, fr_lcoal = normalize_on to
711
+ file_utils.mv(fr_full, to_full, :verbose=>true, :noop=>@noop)
712
+ end
713
+ def from path
714
+ @from_path = path
715
+ end
716
+ def mkdir_p_unless_exists dir_basename=nil
717
+ if dir_basename
718
+ full, short = normalize_on(dir_basename)
719
+ else
720
+ full, short = @on_path, @on_path
721
+ end
722
+ if File.exist?(full)
723
+ report_action :exists, short
724
+ else
725
+ file_utils.mkdir_p(full, :verbose => true, :noop=>@noop)
726
+ end
727
+ end
728
+ alias_method :mkdir_p, :mkdir_p_unless_exists # temporary!!
729
+ def remove path
730
+ full, _ = normalize_on(path)
731
+ file_utils.rm(full, :verbose => true, :noop => @noop)
732
+ end
733
+ def rm_rf_if_exist
734
+ path = @on_path
735
+ if File.exist?(path) && rm_rf_sanity_check(path)
736
+ file_utils.remove_entry_secure(@on_path)
737
+ end
738
+ end
739
+ alias_method :rm_rf, :rm_rf_if_exist # for now
740
+ def run opts={}
741
+ @opts = @opts.merge(opts.reject{|k,v| v.nil?}) # not sure about this
742
+ self.instance_eval(&@block)
743
+ end
744
+ def prefix
745
+ @file_utils.prefix
746
+ end
747
+ def ui
748
+ @file_utils.ui
749
+ end
750
+ def write path, content
751
+ out_path, short = normalize_on path
752
+ content = Treebis.unindent(content) if @unindent
753
+ action = if File.exist? out_path
754
+ File.read(out_path) == content ? :identical :
755
+ ( @overwrite ? :changed : :"won't overwrite" )
756
+ else
757
+ :wrote
758
+ end
759
+ xtra = nil
760
+ if [:changed, :wrote].include?(action)
761
+ b = fh = nil
762
+ File.open(out_path,'w'){|fh| b = fh.write(content)}
763
+ xtra = "(#{b} bytes)"
764
+ end
765
+ report_action action, short, xtra
766
+ end
767
+ private
768
+ def color?; Config.color? end
769
+ def fail(*a); raise ::Treebis::Fail.new(*a) end
770
+ def normalize_from path
771
+ fail("expecting leading dot: #{path}") unless short = undot(path)
772
+ full = File.join(@from_path, short)
773
+ [full, short]
774
+ end
775
+ def normalize_on path
776
+ short = (thing = undot(path)) ? thing : path
777
+ full = File.join(@on_path, short)
778
+ [full, short]
779
+ end
780
+ def patch_hack diff_file
781
+ /\A(.+)\.diff\Z/ =~ diff_file
782
+ use = $1 or fail("diff_file parse fail: #{diff_file}")
783
+ copy use
784
+ end
785
+ def pretty_puts_apply out, cmd
786
+ report_action :exec, cmd.join(' ')
787
+ these = out.split("\n")
788
+ these.each do |this|
789
+ if /^\Apatching file (.+)\Z/ =~ this
790
+ report_action :patched, $1
791
+ else
792
+ ui.puts("#{prefix}#{this}")
793
+ end
794
+ end
795
+ end
796
+ # see also notice()
797
+ def report_action reason, msg=nil, xtra=nil
798
+ reason_s = stylize(reason.to_s, reason)
799
+ puts = ["#{prefix}#{reason_s}", msg, xtra].compact.join(' ')
800
+ ui.puts puts
801
+ end
802
+ def rm_rf_sanity_check path
803
+ # not sure what we want here
804
+ fail('no') if @on_path.nil? || path != @on_path
805
+ true
806
+ end
807
+ def stylize str, reason
808
+ return str unless Config.color?
809
+ colorize(reason.to_s, * get_style(reason))
810
+ end
811
+ def undot path
812
+ $1 if /^(?:\.\/)?(.+)$/ =~ path
813
+ end
814
+ class << self
815
+ # this is not for general use
816
+ def glob_to_regex glob
817
+ fail("this is really strict: (only) 1 '*': #{glob.inspect}") unless
818
+ glob.scan(/\*/).size == 1
819
+ a, b = glob.split('*')
820
+ re = Regexp.new(
821
+ '\\A(.*'+Regexp.escape(a)+')(.*?)('+Regexp.escape(b)+')\\Z'
822
+ )
823
+ re
824
+ end
825
+ end
826
+ end
827
+ end
828
+ end
829
+
830
+ # Experimental extension for tests running tests with a persistent tempdir
831
+ # now you can set and get general properties, and delegate this behavior.
832
+ #
833
+ module Treebis
834
+ module PersistentDotfile
835
+ class << self
836
+ def extend_to(tgt, dotfile_path, opts={})
837
+ opts = {:file_utils=>FileUtils, :dotfile_path=>dotfile_path}.
838
+ merge(opts)
839
+ tgt.extend ClassMethods
840
+ tgt.persistent_dotfile_init opts
841
+ end
842
+ def include_to(mod, *a)
843
+ extend_to(mod, *a)
844
+ mod.send(:include, InstanceMethods)
845
+ end
846
+ end
847
+ DelegatedMethods = %w(tmpdir empty_tmpdir persistent_set persistent_get)
848
+ module ClassMethods
849
+ attr_accessor :dotfile_path, :file_utils
850
+
851
+ def persistent_dotfile_init opts
852
+ @dotfile_path ||= opts[:dotfile_path]
853
+ @file_utils ||= opts[:file_utils]
854
+ @persistent_struct ||= nil
855
+ @tmpdir ||= nil
856
+ end
857
+
858
+ #
859
+ # if it exists delete it. create it. file_utils must be defined
860
+ # @return path to new empty directory
861
+ #
862
+ def empty_tmpdir path, futils_opts={}
863
+ futils_opts = {:verbose=>true}.merge(futils_opts)
864
+ full_path = File.join(tmpdir, path)
865
+ if File.exist? full_path
866
+ file_utils.remove_entry_secure full_path, futils_opts
867
+ end
868
+ file_utils.mkdir_p full_path, futils_opts
869
+ full_path
870
+ end
871
+
872
+ # Get a path to a temporary directory, suitable to be used in tests.
873
+ # The contents of this directory are undefined, but it is writable
874
+ # and as the name implies temporary so a given test should feel free
875
+ # to erase it and create a new empty one at this same path if desired.
876
+ # (see callee for details.)
877
+ #
878
+ def tmpdir
879
+ if @tmpdir.nil?
880
+ @tmpdir = get_tmpdir
881
+ elsif ! File.exist?(@tmpdir) # check every time!
882
+ @tmpdir = get_tmpdir
883
+ end
884
+ @tmpdir
885
+ end
886
+
887
+ def persistent_delegate_to(mod)
888
+ if instance_variable_defined?('@persistent_delegate') # rcov
889
+ @persistent_delegate
890
+ else
891
+ @persistent_delegate = begin
892
+ mm = Module.new
893
+ str = "#{self}::PersistentDotfileDelegate"
894
+ const_set('PersistentDotfileDelegate', mm)
895
+ class << mm; self end.send(:define_method,:inspect){str}
896
+ buck = self
897
+ DelegatedMethods.each do |meth|
898
+ mm.send(:define_method, meth){|*a| buck.send(meth,*a) }
899
+ end
900
+ mm
901
+ end
902
+ end
903
+ mod.extend(@persistent_delegate)
904
+ mod.send(:include, @persistent_delegate)
905
+ end
906
+
907
+ def persistent_get path
908
+ struct = persistent_struct or return nil
909
+ struct[path]
910
+ end
911
+
912
+ # this might cause bugs if different classes use the same
913
+ # dotfile name
914
+ def persistent_struct
915
+ if @persistent_struct
916
+ @persistent_struct
917
+ elsif @persistent_struct == false
918
+ nil
919
+ elsif File.exists? dotfile_path
920
+ @persistent_struct = JSON.parse(File.read(dotfile_path))
921
+ else
922
+ @persistent_struct = false
923
+ end
924
+ end
925
+
926
+ def persistent_set path, value
927
+ struct = persistent_struct || (@persistent_struct = {})
928
+ struct[path] = value
929
+ File.open(dotfile_path, 'w+') do |fh|
930
+ fh.write JSON.pretty_generate(struct)
931
+ end
932
+ nil
933
+ end
934
+
935
+ private
936
+
937
+ # Return the same tmpdir used in the previous run, if a "./treebis.json"
938
+ # file exists in the cwd and it refers to a temp folder that still
939
+ # exists. Else make a new temp folder and write its location to this
940
+ # file. Using the same tempfolder on successive runs is one way to
941
+ # allow us to look at the files it generates between runs.
942
+ #
943
+ def get_tmpdir
944
+ tmpdir = nil
945
+ if tmpdir_path = persistent_get('tmpdir')
946
+ if File.exist?(tmpdir_path)
947
+ tmpdir = tmpdir_path
948
+ end
949
+ end
950
+ unless tmpdir
951
+ tmpdir = Dir::tmpdir + '/treebis'
952
+ file_utils.mkdir_p(tmpdir,:verbose=>true)
953
+ persistent_set 'tmpdir', tmpdir
954
+ end
955
+ tmpdir
956
+ end
957
+ end
958
+
959
+ module InstanceMethods
960
+ DelegatedMethods.each do |this|
961
+ define_method(this){ |*a| self.class.send(this, *a) }
962
+ end
963
+ end
964
+ end
965
+ end
966
+
967
+
968
+
969
+ if [__FILE__, '/usr/bin/rcov'].include?($PROGRAM_NAME) # ick
970
+ # fakefs stuff removed in ac877, see notes there for why
971
+ require 'test/unit'
972
+ require 'test/unit/ui/console/testrunner'
973
+
974
+ module ::Treebis::Test
975
+
976
+ module TestAntecedents
977
+ def setup_antecedents
978
+ src = empty_tmpdir('sourceis')
979
+ task.new do
980
+ mkdir_p "foo/bar/baz"
981
+ write "foo/bar/baz/alpha.txt", 'x'
982
+ write "foo/bar/beta.txt", 'x'
983
+ write "foo/gamma.txt", 'x'
984
+ end.on(src).run
985
+ src
986
+ end
987
+ def test_antecedents
988
+ src = setup_antecedents
989
+ tt = task.new do
990
+ from src
991
+ mkdir_p "foo/bar/baz"
992
+ copy "foo/bar/baz/alpha.txt"
993
+ copy "foo/bar/beta.txt"
994
+ copy "foo/gamma.txt"
995
+ end
996
+ tgt = empty_tmpdir('targetis')
997
+ bb, cc, aa = capture3{ tt.on(tgt).run }
998
+ assert_equal [nil, ''], [aa,bb]
999
+ penu, last = cc.split("\n")[-2..-1]
1000
+ assert penu.index("...bar/beta.txt foo/bar/beta.txt")
1001
+ assert last.index("...eis/foo/gamma.txt foo/gamma.txt")
1002
+ end
1003
+
1004
+ def test_antecedents_raw
1005
+ fu = file_utils
1006
+ src = setup_antecedents
1007
+ tgt = empty_tmpdir('targetis')
1008
+ these = %w( foo/bar/baz/alpha.txt
1009
+ foo/bar/beta.txt
1010
+ foo/gamma.txt )
1011
+ fu.pretty!
1012
+ out, err = capture3 do
1013
+ fu.mkdir_p File.join(tgt,'foo/bar/baz')
1014
+ these.each do |foo|
1015
+ from, to = File.join(src,foo), File.join(tgt,foo)
1016
+ fu.cp from, to
1017
+ end
1018
+ end
1019
+ assert_equal '', out
1020
+ penu, last = err.split("\n")[-2..-1]
1021
+ assert penu.index(' ...bar/beta.txt ...bar/beta.txt')
1022
+ assert last.index('...eis/foo/gamma.txt ...tis/foo/gamma.txt')
1023
+ end
1024
+ end
1025
+
1026
+ module TestColorAndRmAndMoveAndPatch
1027
+ def test_rm_rf
1028
+ t = task.new do
1029
+ rm_rf
1030
+ mkdir_p
1031
+ write('foo.txt', "hi i'm foo")
1032
+ end
1033
+ out = tmpdir+'/blearg'
1034
+ t.on(out).run
1035
+ t.on(out).run
1036
+ tree = dir_as_hash(out)
1037
+ assert_equal({"foo.txt"=>"hi i'm foo"}, tree)
1038
+ end
1039
+ def test_no_color
1040
+ t = task.new do
1041
+ report_action :fake, "blah"
1042
+ end
1043
+ Treebis::Config.no_color!
1044
+ out = t.ui_capture{ on('x').run }
1045
+
1046
+ Treebis::Config.color!
1047
+ assert_equal(" fake blah\n", out)
1048
+ end
1049
+ def test_no_overwrite
1050
+ dir = tmpdir + '/no-overwrite/'
1051
+ Fu.remove_entry_secure(dir) if File.exist?(dir)
1052
+ t = task.new do
1053
+ mkdir_p_unless_exists
1054
+ write 'foo.txt','blah content'
1055
+ end
1056
+ t2 = task.new do
1057
+ write 'foo.txt','blah content again'
1058
+ end
1059
+ str1 = t.ui_capture{ on(dir).run }
1060
+ str2 = t2.ui_capture{ on(dir).run }
1061
+ assert_match( /wrote/, str1 )
1062
+ assert_match( /won/, str2 )
1063
+ end
1064
+ def setup_sourcedir
1065
+ src_dir = tmpdir+'/banana'
1066
+ # return if File.exist?(src_dir)
1067
+ task = Treebis::Task.new do
1068
+ rm_rf
1069
+ mkdir_p
1070
+ mkdir_p 'dir1'
1071
+ write 'dir1/stooges.txt.diff', <<-FILE
1072
+ --- a/stooges.txt 2010-04-25 03:23:18.000000000 -0400
1073
+ +++ b/stooges.txt 2010-04-25 03:23:12.000000000 -0400
1074
+ @@ -1,2 +1,3 @@
1075
+ larry
1076
+ +moe
1077
+ curly
1078
+ FILE
1079
+
1080
+ write 'stooges.orig.txt', <<-FILE
1081
+ larry
1082
+ curly
1083
+ FILE
1084
+
1085
+ write 'treebis-task.rb', <<-FILE
1086
+ Treebis.tasks.task(:banana) do
1087
+ copy './stooges.orig.txt'
1088
+ mkdir_p_unless_exists './dir1'
1089
+ move './stooges.orig.txt', './dir1/stooges.txt'
1090
+ apply './dir1/stooges.txt.diff'
1091
+ end
1092
+ FILE
1093
+ end
1094
+ task.on(src_dir).run
1095
+ end
1096
+ def test_move
1097
+ setup_sourcedir
1098
+ task = Treebis.dir_task(tmpdir+'/banana')
1099
+ out_dir = tmpdir+'/test-move-output'
1100
+ file_utils.remove_entry_secure(out_dir) if File.exist?(out_dir)
1101
+ file_utils.mkdir_p(out_dir)
1102
+ task.on(out_dir).run
1103
+ tree = dir_as_hash(out_dir)
1104
+ assert_equal({"dir1"=>{"stooges.txt"=>"larry\nmoe\ncurly\n"}}, tree)
1105
+ end
1106
+ end
1107
+
1108
+ module TestColorize
1109
+ def test_colorize
1110
+ obj = Object.new
1111
+ obj.extend Treebis::Colorize
1112
+ str = obj.send(:colorize, "foo",:background, :blue)
1113
+ assert_equal "\e[44mfoo\e[0m", str
1114
+ end
1115
+ end
1116
+
1117
+ module TestDirAsHash
1118
+ def test_baby_jug_fail
1119
+ hh = {'foo'=>nil}
1120
+ e = assert_raise(RuntimeError) do
1121
+ hash_to_dir(hh, empty_tmpdir('babyjug')+'/bar', file_utils)
1122
+ end
1123
+ assert_match(/bad type for dir hash/, e.message)
1124
+ end
1125
+ #
1126
+ # http://www.youtube.com/watch?v=Ib0Tll3sGB0
1127
+ #
1128
+ def test_dir_as_hash
1129
+ h = {
1130
+ 'not'=>'funny',
1131
+ 'baby-jug'=>{
1132
+ 'what-does'=>'the baby have',
1133
+ 'blood'=>'where?'},
1134
+ 'not-funny'=>{
1135
+ 'youre' => {
1136
+ 'right' => 'its not funny',
1137
+ 'not'=>{
1138
+ 'funny'=>'ha ha',
1139
+ 'not funny'=>'BLOO-DUH'
1140
+ }
1141
+ }
1142
+ }
1143
+ }
1144
+ td = empty_tmpdir('blah')+'/not-there'
1145
+ hash_to_dir(h, td, file_utils)
1146
+ skips = [ 'baby-jug/blood',
1147
+ 'not-funny/youre/not/funny']
1148
+ wow = dir_as_hash(td, :skip=>skips)
1149
+ no_way = {
1150
+ "baby-jug"=>{"what-does"=>"the baby have"},
1151
+ "not"=>"funny",
1152
+ "not-funny"=>{
1153
+ "youre"=>{
1154
+ "right"=>"its not funny",
1155
+ "not"=>{"not funny"=>"BLOO-DUH"}
1156
+ }
1157
+ }
1158
+ }
1159
+ assert_equal no_way, wow
1160
+ end
1161
+ def test_dir_as_hash_with_globs
1162
+ h = {
1163
+ 'file1.txt' => 'foo',
1164
+ 'file2.rb' => 'baz',
1165
+ 'dir1' => {
1166
+ 'file3.txt' => 'bar',
1167
+ 'file4.rb' => 'bling'
1168
+ }
1169
+ }
1170
+ td = empty_tmpdir('blah')+'/not-there'
1171
+ hash_to_dir(h, td, file_utils)
1172
+ skips = [ 'dir1/*.rb' ]
1173
+ wow = dir_as_hash(td, :skip=>skips)
1174
+ no_way = {
1175
+ "file1.txt"=>"foo", "file2.rb"=>"baz",
1176
+ "dir1"=>{"file3.txt"=>"bar"},
1177
+ }
1178
+ assert_equal no_way, wow
1179
+ end
1180
+ end
1181
+
1182
+ module TestFileUtilsProxy
1183
+ def test_cp_not_pretty
1184
+ src_dir = empty_tmpdir('foo')
1185
+ tgt_dir = empty_tmpdir('bar')
1186
+
1187
+ task.new{
1188
+ mkdir_p
1189
+ write 'some-file.txt', <<-X
1190
+ i am some text
1191
+ X
1192
+ }.on(src_dir).run
1193
+
1194
+ task.new {
1195
+ from(src_dir)
1196
+ report_action :notice, "the below is not supposed to be pretty"
1197
+ file_utils.not_pretty!
1198
+ copy 'some-file.txt'
1199
+ mkdir_p 'emptoz'
1200
+ file_utils.pretty!
1201
+ report_action :notice, "the above was not pretty"
1202
+ }.on(tgt_dir).run
1203
+
1204
+ act = dir_as_hash tgt_dir
1205
+ exp = {
1206
+ "some-file.txt"=>"i am some text\n",
1207
+ "emptoz" => {}
1208
+ }
1209
+ assert_equal(exp, act)
1210
+ end
1211
+ def test_patch_fileutils
1212
+ tmpdir = empty_tmpdir('oilspill')
1213
+ out, err = capture3 do
1214
+ file_utils.remove_entry_secure tmpdir+'/not-there'
1215
+ file_utils.mkdir_p tmpdir, :verbose => true
1216
+ end
1217
+ assert_match(/didn't exist.+not-there/, err)
1218
+ assert_match(/mkdir_p.+exists.+oilspill/, err)
1219
+ end
1220
+ def test_pretty_puts_cp_unexpected_output
1221
+ e = assert_raises(RuntimeError) do
1222
+ file_utils.send(:pretty_puts_cp, false, '', '')
1223
+ end
1224
+ assert_match(/unexpected out/, e.message)
1225
+ end
1226
+ def test_custom_fileutils_implementation
1227
+ task = self.task.new do
1228
+ from 'not there'
1229
+ copy 'doesnt even exist'
1230
+ end
1231
+ fu = Object.new
1232
+ class << fu
1233
+ def prefix; '_____' end
1234
+ def ui; $stdout end
1235
+ end
1236
+ hi_there = false
1237
+ class << fu; self end.send(:define_method, :cp) do |*blah|
1238
+ hi_there = blah
1239
+ end
1240
+ task.file_utils = fu
1241
+ task.on('doesnt exist here').run
1242
+ exp =
1243
+ ["not there/doesnt even exist", "doesnt exist here/doesnt even exist"]
1244
+ act = hi_there[0..1]
1245
+ assert_equal exp, act
1246
+ end
1247
+ end
1248
+
1249
+ module TestGlobMatcher
1250
+ def test_glob_matcher
1251
+ obj = Object.new
1252
+ obj.extend Treebis::DirAsHash::Blacklist::MatcherFactory
1253
+ err = assert_raises(RuntimeError) do
1254
+ obj.create_matcher(false)
1255
+ end
1256
+ assert_equal "can't build matcher from false", err.message
1257
+ end
1258
+ end
1259
+
1260
+ module TestPatch
1261
+ def test_patch_fails
1262
+ src = empty_tmpdir 'patch/evil'
1263
+ task.new do
1264
+ write "badly-formed-patch-file.diff", <<-P
1265
+ i am not a good patch
1266
+ P
1267
+ end.on(src).run
1268
+
1269
+ tgt = empty_tmpdir 'patch/innocent'
1270
+ patch_task = task.new do
1271
+ from src
1272
+ apply 'badly-formed-patch-file.diff'
1273
+ end
1274
+ e = assert_raises(Treebis::Fail) do
1275
+ patch_task.on(tgt).run
1276
+ end
1277
+ assert_match(/Failed to run patch command/, e.message)
1278
+ end
1279
+ def test_patch_hack
1280
+ task.new {
1281
+ write "some-file.txt", <<-X
1282
+ i am the new content
1283
+ X
1284
+ }.on(src = empty_tmpdir('src')).run
1285
+ task.new {
1286
+ write "some-file.txt", <<-X
1287
+ never see me
1288
+ X
1289
+ }.on(tgt = empty_tmpdir('tgt')).run
1290
+ task.new {
1291
+ from src
1292
+ opts[:patch_hack] = true
1293
+ apply "some-file.txt.diff"
1294
+ }.on(tgt).run
1295
+ assert_equal(
1296
+ {"some-file.txt"=>"i am the new content\n"},
1297
+ dir_as_hash(tgt)
1298
+ )
1299
+ end
1300
+ def test_unexpected_string_from_patch
1301
+ out, err, res = capture3 do
1302
+ task.new do
1303
+ pretty_puts_apply("i am another string", [])
1304
+ nil
1305
+ end.on(empty_tmpdir("blah")).run
1306
+ end
1307
+ assert_equal [nil, ""], [res, out]
1308
+ assert err.index("i am another string")
1309
+ end
1310
+ end
1311
+
1312
+ module TestPersistentDotfile
1313
+ def test_persistent_dotfile_empty_tempdir
1314
+ blah1 = empty_tmpdir('blah/blah')
1315
+ blah2 = empty_tmpdir('blah/blah')
1316
+ assert_equal(blah1, blah2)
1317
+ assert_equal({}, dir_as_hash(blah2))
1318
+ end
1319
+ def test_delegate_to
1320
+ parent_mod = Module.new
1321
+ Treebis::PersistentDotfile.extend_to(
1322
+ parent_mod, TestCase::DotfilePath
1323
+ )
1324
+ child_mod = Module.new
1325
+ child_mod2 = Module.new
1326
+ parent_mod.persistent_delegate_to(child_mod)
1327
+ parent_mod.persistent_delegate_to(child_mod2)
1328
+ foo = parent_mod.empty_tmpdir('bliz')
1329
+ bar = child_mod.empty_tmpdir('bliz')
1330
+ baz = child_mod2.empty_tmpdir('bliz')
1331
+ assert_equal(foo, bar)
1332
+ assert_equal(foo, baz)
1333
+ end
1334
+ end
1335
+
1336
+ module TestRemove
1337
+ def test_remove_pretty
1338
+ task.new {
1339
+ write "foo.txt", "bar baz"
1340
+ }.on(tgt = empty_tmpdir("foo")).run
1341
+
1342
+ assert_equal({"foo.txt"=>"bar baz"}, dir_as_hash(tgt))
1343
+
1344
+ task.new {
1345
+ remove "foo.txt"
1346
+ }.on(tgt).run
1347
+
1348
+ assert_equal({}, dir_as_hash(tgt))
1349
+ end
1350
+
1351
+ def test_remove_not_pretty
1352
+ task.new {
1353
+ write "foo.txt", "bar baz"
1354
+ }.on(tgt = empty_tmpdir("foo")).run
1355
+
1356
+ assert_equal({"foo.txt"=>"bar baz"}, dir_as_hash(tgt))
1357
+
1358
+ bb, cc, aa = capture3{
1359
+ tt = task.new do
1360
+ file_utils.not_pretty!
1361
+ remove "foo.txt"
1362
+ end
1363
+ tt.on(tgt).run
1364
+ }
1365
+ assert_match(/\Arm /, cc)
1366
+ assert aa.kind_of?(Array)
1367
+ assert_equal 1, aa.size
1368
+ assert_equal "", bb
1369
+ assert_equal({}, dir_as_hash(tgt))
1370
+ end
1371
+ end
1372
+
1373
+
1374
+ module TestTaskMisc
1375
+ def setup_notice_task
1376
+ @task = task.new{notice("hello","HI"); 'foo'}
1377
+ end
1378
+ def assert_notice_outcome
1379
+ @out, @err, @res = capture3{ @task.on(nil).run }
1380
+ assert_equal ['', 'foo'], [@out, @res]
1381
+ assert_match(/hello.*HI/, @err)
1382
+ end
1383
+ def test_notice_color
1384
+ setup_notice_task
1385
+ assert_notice_outcome
1386
+ assert @err.index("\e[")
1387
+ end
1388
+ def test_notice_no_color
1389
+ cfg = Treebis::Config
1390
+ prev = cfg.color?
1391
+ cfg.no_color!
1392
+ begin
1393
+ setup_notice_task
1394
+ assert_notice_outcome
1395
+ assert @err.index('hello HI')
1396
+ ensure
1397
+ prev ? cfg.color! : cfg.no_color!
1398
+ end
1399
+ end
1400
+ end
1401
+
1402
+ module TestSopen
1403
+ def test_sopen2_fails
1404
+ e = assert_raises(RuntimeError) do
1405
+ Treebis::Sopen2.sopen2assert('patch', '-u', 'foo', 'bar')
1406
+ end
1407
+ assert_match(/Can't open patch file bar/, e.message)
1408
+ end
1409
+ def test_sopen2_succeeds
1410
+ out = Treebis::Sopen2.sopen2assert('echo', 'baz')
1411
+ assert_equal "baz\n", out
1412
+ end
1413
+ end
1414
+
1415
+ module TestTempdirAndTasksAndCopy
1416
+ def test_cant_reopen_tasks
1417
+ tasks = Treebis::TaskSet.new
1418
+ tasks.task(:same){}
1419
+ e = assert_raises(RuntimeError) do
1420
+ tasks.task(:same){}
1421
+ end
1422
+ exp = "can't reopen task: :same"
1423
+ assert_equal(exp, e.message)
1424
+ end
1425
+ def test_can_open_same_name_in_different_task_sets
1426
+ Treebis::TaskSet.new.task(:same){}
1427
+ Treebis::TaskSet.new.task(:same){}
1428
+ end
1429
+ def test_copy_one_file_nothing_exist
1430
+ out_dir = tmpdir+'/out-dir'
1431
+ src_file = tmpdir+'/baz.txt'
1432
+ per_file = self.class.dotfile_path
1433
+ file_utils.remove_entry_secure(out_dir) if File.exist?(out_dir)
1434
+ file_utils.remove_entry_secure(src_file) if File.exist?(src_file)
1435
+ file_utils.remove_entry_secure(per_file) if File.exist?(per_file)
1436
+ test_copy_one_file
1437
+ end
1438
+ def test_copy_one_file_almost_nothing_exist
1439
+ out_dir = tmpdir+'/out-dir'
1440
+ src_file = tmpdir+'/baz.txt'
1441
+ per_file = self.class.dotfile_path
1442
+ file_utils.remove_entry_secure(out_dir) if File.exist?(out_dir)
1443
+ file_utils.remove_entry_secure(src_file) if File.exist?(src_file)
1444
+ if File.exist?(per_file)
1445
+ struct = JSON.parse(File.read(per_file))
1446
+ if File.exist?(struct["tmpdir"])
1447
+ file_utils.remove_entry_secure(struct["tmpdir"])
1448
+ end
1449
+ end
1450
+ test_copy_one_file
1451
+ end
1452
+ def test_copy_one_file_a_bunch_of_tmpdir_crap
1453
+ out_dir = tmpdir+'/out-dir'
1454
+ src_file = tmpdir+'/baz.txt'
1455
+ file_utils.remove_entry_secure(out_dir) if File.exist?(out_dir)
1456
+ file_utils.remove_entry_secure(src_file) if File.exist?(src_file)
1457
+ self.class.instance_variable_set('@tmpdir',nil)
1458
+ tmpdir # gets to a hard to reach line
1459
+ test_copy_one_file
1460
+ end
1461
+ def test_copy_one_file
1462
+ the_tmpdir = tmpdir
1463
+ # doc start
1464
+ content = "i am the content of\na file called baz.txt\n"
1465
+ File.open(the_tmpdir+'/baz.txt','w+') do |fh|
1466
+ fh.puts(content)
1467
+ end
1468
+ tasks = Treebis::TaskSet.new
1469
+ tasks.task(:default) do
1470
+ mkdir_p_unless_exists # makes output dir referred to in on() below
1471
+ from the_tmpdir # the source directory
1472
+ copy('./baz.txt')
1473
+ end
1474
+ tasks[:default].on(the_tmpdir+'/out-dir/').run()
1475
+ output = File.read(the_tmpdir+'/out-dir/baz.txt')
1476
+ # doc stop
1477
+ assert_equal(content, output)
1478
+ end
1479
+ end
1480
+
1481
+ class TestCase < ::Test::Unit::TestCase
1482
+ DotfilePath = './treebis.persistent.json'
1483
+ include Treebis::DirAsHash, Treebis::Capture3
1484
+ include TestAntecedents
1485
+ include TestColorAndRmAndMoveAndPatch
1486
+ include TestColorize
1487
+ include TestDirAsHash
1488
+ include TestFileUtilsProxy
1489
+ include TestGlobMatcher
1490
+ include TestPatch
1491
+ include TestPersistentDotfile
1492
+ include TestRemove
1493
+ include TestTaskMisc
1494
+ include TestTempdirAndTasksAndCopy
1495
+ include TestSopen
1496
+
1497
+ futils_prefix = sprintf( "%s%s --> ", Treebis::Config.default_prefix,
1498
+ Treebis::Colorize.colorize('for test:', :bright, :blue) )
1499
+
1500
+ file_utils = Treebis::Config.new_default_file_utils_proxy
1501
+ file_utils.prefix = futils_prefix
1502
+
1503
+ Treebis::PersistentDotfile.include_to( self,
1504
+ DotfilePath, :file_utils => file_utils )
1505
+
1506
+ @file_utils = file_utils
1507
+ define_method( :file_utils ){ file_utils }
1508
+ alias_method :fu, :file_utils
1509
+ class << self
1510
+ attr_accessor :file_utils
1511
+ end
1512
+ def task; Treebis::Task end
1513
+ end
1514
+ ::Test::Unit::UI::Console::TestRunner.run(TestCase)
1515
+ end
1516
+ end