treebis 0.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.
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