shanel-autotest 4.2.3

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/autotest.rb ADDED
@@ -0,0 +1,676 @@
1
+ require 'find'
2
+ require 'rbconfig'
3
+
4
+ $TESTING = false unless defined? $TESTING
5
+
6
+ ##
7
+ # Autotest continuously scans the files in your project for changes
8
+ # and runs the appropriate tests. Test failures are run until they
9
+ # have all passed. Then the full test suite is run to ensure that
10
+ # nothing else was inadvertantly broken.
11
+ #
12
+ # If you want Autotest to start over from the top, hit ^C once. If
13
+ # you want Autotest to quit, hit ^C twice.
14
+ #
15
+ # Rails:
16
+ #
17
+ # The autotest command will automatically discover a Rails directory
18
+ # by looking for config/environment.rb. When Rails is discovered,
19
+ # autotest uses RailsAutotest to perform file mappings and other work.
20
+ # See RailsAutotest for details.
21
+ #
22
+ # Plugins:
23
+ #
24
+ # Plugins are available by creating a .autotest file either in your
25
+ # project root or in your home directory. You can then write event
26
+ # handlers in the form of:
27
+ #
28
+ # Autotest.add_hook hook_name { |autotest| ... }
29
+ #
30
+ # The available hooks are listed in +ALL_HOOKS+.
31
+ #
32
+ # See example_dot_autotest.rb for more details.
33
+ #
34
+ # If a hook returns a true value, it signals to autotest that the hook
35
+ # was handled and should not continue executing hooks.
36
+ #
37
+ # Naming:
38
+ #
39
+ # Autotest uses a simple naming scheme to figure out how to map
40
+ # implementation files to test files following the Test::Unit naming
41
+ # scheme.
42
+ #
43
+ # * Test files must be stored in test/
44
+ # * Test files names must start with test_
45
+ # * Test class names must start with Test
46
+ # * Implementation files must be stored in lib/
47
+ # * Implementation files must match up with a test file named
48
+ # test_.*implementation.rb
49
+ #
50
+ # Strategy:
51
+ #
52
+ # 1. Find all files and associate them from impl <-> test.
53
+ # 2. Run all tests.
54
+ # 3. Scan for failures.
55
+ # 4. Detect changes in ANY (ruby?. file, rerun all failures + changed files.
56
+ # 5. Until 0 defects, goto 3.
57
+ # 6. When 0 defects, goto 2.
58
+
59
+ class Autotest
60
+
61
+ VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
62
+
63
+ T0 = Time.at 0
64
+
65
+ ALL_HOOKS = [ :all_good, :died, :green, :initialize, :interrupt, :quit,
66
+ :ran_command, :red, :reset, :run_command, :updated, :waiting ]
67
+
68
+ @@options = {}
69
+ def self.options;@@options;end
70
+ def options;@@options;end
71
+
72
+
73
+ HOOKS = Hash.new { |h,k| h[k] = [] } #unfound keys are []
74
+ unless defined? WINDOZE then
75
+ WINDOZE = /win32/ =~ RUBY_PLATFORM
76
+ SEP = WINDOZE ? '&' : ';'
77
+ end
78
+
79
+ @@discoveries = []
80
+
81
+ ##
82
+ # Add a proc to the collection of discovery procs. See
83
+ # +autodiscover+.
84
+
85
+ def self.add_discovery &proc
86
+ @@discoveries << proc
87
+ end
88
+
89
+ ##
90
+ # Automatically find all potential autotest runner styles by
91
+ # searching your loadpath, vendor/plugins, and rubygems for
92
+ # "autotest/discover.rb". If found, that file is loaded and it
93
+ # should register discovery procs with autotest using
94
+ # +add_discovery+. That proc should return one or more strings
95
+ # describing the user's current environment. Those styles are then
96
+ # combined to dynamically invoke an autotest plugin to suite your
97
+ # environment. That plugin should define a subclass of Autotest with
98
+ # a corresponding name.
99
+ #
100
+ # === Process:
101
+ #
102
+ # 1. All autotest/discover.rb files loaded.
103
+ # 2. Those procs determine your styles (eg ["rails", "rspec"]).
104
+ # 3. Require file by sorting styles and joining (eg 'autotest/rails_rspec').
105
+ # 4. Invoke run method on appropriate class (eg Autotest::RailsRspec.run).
106
+ #
107
+ # === Example autotest/discover.rb:
108
+ #
109
+ # Autotest.add_discovery do
110
+ # "rails" if File.exist? 'config/environment.rb'
111
+ # end
112
+ #
113
+ def self.autodiscover
114
+ require 'rubygems'
115
+
116
+ Gem.find_files("autotest/discover").each do |f|
117
+ load f
118
+ end
119
+
120
+ #call all discover procs an determine style
121
+ @@discoveries.map{ |proc| proc.call }.flatten.compact.sort.uniq
122
+ end
123
+
124
+ ##
125
+ # Initialize and run the system.
126
+
127
+ def self.run
128
+ new.run
129
+ end
130
+
131
+ attr_writer :known_files
132
+ attr_accessor(:completed_re,
133
+ :extra_class_map,
134
+ :extra_files,
135
+ :failed_results_re,
136
+ :files_to_test,
137
+ :find_order,
138
+ :interrupted,
139
+ :last_mtime,
140
+ :libs,
141
+ :order,
142
+ :output,
143
+ :results,
144
+ :sleep,
145
+ :tainted,
146
+ :testlib,
147
+ :find_directories,
148
+ :unit_diff,
149
+ :wants_to_quit)
150
+
151
+ ##
152
+ # Initialize the instance and then load the user's .autotest file, if any.
153
+
154
+ def initialize
155
+ # these two are set directly because they're wrapped with
156
+ # add/remove/clear accessor methods
157
+ @exception_list = []
158
+ @test_mappings = []
159
+
160
+ self.completed_re = /\d+ tests, \d+ assertions, \d+ failures, \d+ errors/
161
+ self.extra_class_map = {}
162
+ self.extra_files = []
163
+ self.failed_results_re = /^\s+\d+\) (?:Failure|Error):\n(.*?)\((.*?)\)/
164
+ self.files_to_test = new_hash_of_arrays
165
+ self.find_order = []
166
+ self.known_files = nil
167
+ self.libs = %w[. lib test].join(File::PATH_SEPARATOR)
168
+ self.order = :random
169
+ self.output = $stderr
170
+ self.sleep = 1
171
+ self.testlib = "test/unit"
172
+ self.find_directories = ['.']
173
+ self.unit_diff = "unit_diff -u"
174
+
175
+ #add Test::Unit mappings
176
+ #file in /lib -> run test in /test
177
+ self.add_mapping(/^lib\/.*\.rb$/) do |filename, _|
178
+ possible = File.basename(filename).gsub '_', '_?'
179
+ files_matching %r%^test/.*#{possible}$%
180
+ end
181
+
182
+ #file in /test -> run it
183
+ self.add_mapping(/^test.*\/test_.*rb$/) do |filename, _|
184
+ filename
185
+ end
186
+
187
+ #execute custom extensions
188
+ configs = ['./.autotest']
189
+ if options[:rc]
190
+ configs << File.expand_path(options[:rc])
191
+ else
192
+ configs << File.expand_path('~/.autotest')
193
+ end
194
+ configs.each do |f|
195
+ load f if File.exist? f
196
+ end
197
+ end
198
+
199
+ ##
200
+ # Repeatedly run failed tests, then all tests, then wait for changes
201
+ # and carry on until killed.
202
+
203
+ def run
204
+ hook :initialize
205
+ reset
206
+ add_sigint_handler
207
+
208
+ self.last_mtime = Time.now if options[:no_full_after_start]
209
+
210
+ loop do
211
+ begin # ^c handler
212
+ get_to_green
213
+ if self.tainted and not options[:no_full_after_failed] then
214
+ rerun_all_tests
215
+ else
216
+ hook :all_good
217
+ end
218
+ wait_for_changes
219
+ rescue Interrupt
220
+ break if self.wants_to_quit
221
+ reset
222
+ end
223
+ end
224
+ hook :quit
225
+ rescue Exception => err
226
+ hook :died, err
227
+ end
228
+
229
+ ##
230
+ # Keep running the tests after a change, until all pass.
231
+
232
+ def get_to_green
233
+ begin
234
+ run_tests
235
+ wait_for_changes unless all_good
236
+ end until all_good
237
+ end
238
+
239
+ ##
240
+ # Look for files to test then run the tests and handle the results.
241
+
242
+ def run_tests
243
+ hook :run_command
244
+
245
+ new_mtime = self.find_files_to_test
246
+ return unless new_mtime
247
+ self.last_mtime = new_mtime
248
+
249
+ cmd = self.make_test_cmd self.files_to_test
250
+ return if cmd.empty?
251
+
252
+ puts cmd unless options[:quiet]
253
+
254
+ old_sync = $stdout.sync
255
+ $stdout.sync = true
256
+ self.results = []
257
+ line = []
258
+ begin
259
+ open("| #{cmd}", "r") do |f|
260
+ until f.eof? do
261
+ c = f.getc or break
262
+ print (c.is_a?(Fixnum) ? c.chr : c)
263
+ line << c
264
+ if c == ?\n then
265
+ self.results << if RUBY_VERSION >= "1.9" then
266
+ line.join
267
+ else
268
+ line.pack "c*"
269
+ end
270
+ line.clear
271
+ end
272
+ end
273
+ end
274
+ ensure
275
+ $stdout.sync = old_sync
276
+ end
277
+ hook :ran_command
278
+ self.results = self.results.join
279
+
280
+ handle_results(self.results)
281
+ end
282
+
283
+ ############################################################
284
+ # Utility Methods, not essential to reading of logic
285
+
286
+ ##
287
+ # Installs a sigint handler.
288
+
289
+ def add_sigint_handler
290
+ trap 'INT' do
291
+ if self.interrupted then
292
+ self.wants_to_quit = true
293
+ else
294
+ unless hook :interrupt then
295
+ puts "Interrupt a second time to quit"
296
+ self.interrupted = true
297
+ Kernel.sleep 1.5
298
+ end
299
+ raise Interrupt, nil # let the run loop catch it
300
+ end
301
+ end
302
+ end
303
+
304
+ ##
305
+ # If there are no files left to test (because they've all passed),
306
+ # then all is good.
307
+
308
+ def all_good
309
+ files_to_test.empty?
310
+ end
311
+
312
+ ##
313
+ # Convert a path in a string, s, into a class name, changing
314
+ # underscores to CamelCase, etc.
315
+
316
+ def path_to_classname(s)
317
+ sep = File::SEPARATOR
318
+ f = s.sub(/^test#{sep}/, '').sub(/\.rb$/, '').split(sep)
319
+ f = f.map { |path| path.split(/_|(\d+)/).map { |seg| seg.capitalize }.join }
320
+ f = f.map { |path| path =~ /^Test/ ? path : "Test#{path}" }
321
+ f.join('::')
322
+ end
323
+
324
+ ##
325
+ # Returns a hash mapping a file name to the known failures for that
326
+ # file.
327
+
328
+ def consolidate_failures(failed)
329
+ filters = new_hash_of_arrays
330
+
331
+ class_map = Hash[*self.find_order.grep(/^test/).map { |f| # TODO: ugly
332
+ [path_to_classname(f), f]
333
+ }.flatten]
334
+ class_map.merge!(self.extra_class_map)
335
+
336
+ failed.each do |method, klass|
337
+ if class_map.has_key? klass then
338
+ filters[class_map[klass]] << method
339
+ else
340
+ output.puts "Unable to map class #{klass} to a file"
341
+ end
342
+ end
343
+
344
+ return filters
345
+ end
346
+
347
+ ##
348
+ # Find the files to process, ignoring temporary files, source
349
+ # configuration management files, etc., and return a Hash mapping
350
+ # filename to modification time.
351
+
352
+ def find_files
353
+ result = {}
354
+ targets = self.find_directories + self.extra_files
355
+ self.find_order.clear
356
+
357
+ targets.each do |target|
358
+ order = []
359
+ Find.find(target) do |f|
360
+ Find.prune if f =~ self.exceptions
361
+
362
+ next if test ?d, f
363
+ next if f =~ /(swp|~|rej|orig)$/ # temporary/patch files
364
+ next if f =~ /\/\.?#/ # Emacs autosave/cvs merge files
365
+
366
+ filename = f.sub(/^\.\//, '')
367
+
368
+ result[filename] = File.stat(filename).mtime rescue next
369
+ order << filename
370
+ end
371
+ self.find_order.push(*order.sort)
372
+ end
373
+
374
+ return result
375
+ end
376
+
377
+ ##
378
+ # Find the files which have been modified, update the recorded
379
+ # timestamps, and use this to update the files to test. Returns
380
+ # the latest mtime of the files modified or nil when nothing was
381
+ # modified.
382
+ def find_files_to_test(files=find_files)
383
+ updated = files.select { |filename, mtime| self.last_mtime < mtime }
384
+
385
+ unless updated.empty? or self.last_mtime.to_i == 0 #nothing to update or initial run
386
+ p updated if options[:verbose]
387
+ hook :updated, updated
388
+ end
389
+
390
+ updated.map { |f,m| test_files_for(f) }.flatten.uniq.each do |filename|
391
+ self.files_to_test[filename] # creates key with default value
392
+ end
393
+
394
+ if updated.empty? then
395
+ nil
396
+ else
397
+ files.values.max
398
+ end
399
+ end
400
+
401
+ ##
402
+ # Check results for failures, set the "bar" to red or green, and if
403
+ # there are failures record this.
404
+
405
+ def handle_results(results)
406
+ failed = results.scan(self.failed_results_re)
407
+ completed = results =~ self.completed_re
408
+
409
+ self.files_to_test = consolidate_failures failed if completed
410
+
411
+ color = completed && self.files_to_test.empty? ? :green : :red
412
+ hook color unless $TESTING
413
+
414
+ self.tainted = true unless self.files_to_test.empty?
415
+ end
416
+
417
+ ##
418
+ # Lazy accessor for the known_files hash.
419
+
420
+ def known_files
421
+ unless @known_files then
422
+ @known_files = Hash[*find_order.map { |f| [f, true] }.flatten]
423
+ end
424
+ @known_files
425
+ end
426
+
427
+ ##
428
+ # Generate the commands to test the supplied files
429
+
430
+ def make_test_cmd files_to_test
431
+ cmds = []
432
+ full, partial = reorder(files_to_test).partition { |k,v| v.empty? }
433
+ base_cmd = "#{ruby} -I#{libs} -rubygems"
434
+
435
+ unless full.empty? then
436
+ classes = full.map {|k,v| k}.flatten.uniq
437
+ classes.unshift testlib
438
+ cmds << "#{base_cmd} -e \"%w[#{classes.join(' ')}].each { |f| require f }\" | #{unit_diff}"
439
+ end
440
+
441
+ partial.each do |klass, methods|
442
+ regexp = Regexp.union(*methods).source
443
+ cmds << "#{base_cmd} #{klass} -n \"/^(#{regexp})$/\" | #{unit_diff}"
444
+ end
445
+
446
+ return cmds.join("#{SEP} ")
447
+ end
448
+
449
+ def new_hash_of_arrays
450
+ Hash.new { |h,k| h[k] = [] }
451
+ end
452
+
453
+ def reorder files_to_test
454
+ case self.order
455
+ when :alpha then
456
+ files_to_test.sort_by { |k,v| k }
457
+ when :reverse then
458
+ files_to_test.sort_by { |k,v| k }.reverse
459
+ when :random then
460
+ max = files_to_test.size
461
+ files_to_test.sort_by { |k,v| rand(max) }
462
+ when :natural then
463
+ (self.find_order & files_to_test.keys).map { |f| [f, files_to_test[f]] }
464
+ else
465
+ raise "unknown order type: #{self.order.inspect}"
466
+ end
467
+ end
468
+
469
+ ##
470
+ # Rerun the tests from cold (reset state)
471
+
472
+ def rerun_all_tests
473
+ reset
474
+ run_tests
475
+
476
+ hook :all_good if all_good
477
+ end
478
+
479
+ ##
480
+ # Clear all state information about test failures and whether
481
+ # interrupts will kill autotest.
482
+
483
+ def reset
484
+ self.files_to_test.clear
485
+ self.find_order.clear
486
+ self.interrupted = false
487
+ self.known_files = nil
488
+ self.last_mtime = T0
489
+ self.tainted = false
490
+ self.wants_to_quit = false
491
+
492
+ hook :reset
493
+ end
494
+
495
+ ##
496
+ # Determine and return the path of the ruby executable.
497
+
498
+ def ruby
499
+ ruby = ENV['RUBY']
500
+ ruby ||= File.join(Config::CONFIG['bindir'],
501
+ Config::CONFIG['ruby_install_name'])
502
+
503
+ ruby.gsub! File::SEPARATOR, File::ALT_SEPARATOR if File::ALT_SEPARATOR
504
+
505
+ return ruby
506
+ end
507
+
508
+ ##
509
+ # Return the name of the file with the tests for filename by finding
510
+ # a +test_mapping+ that matches the file and executing the mapping's
511
+ # proc.
512
+
513
+ def test_files_for(filename)
514
+ result = @test_mappings.find { |file_re, ignored| filename =~ file_re }
515
+
516
+ p :test_file_for => [filename, result.first] if result and $DEBUG
517
+
518
+ result = result.nil? ? [] : [result.last.call(filename, $~)].flatten
519
+
520
+ output.puts "No tests matched #{filename}" if
521
+ (options[:verbose] or $TESTING) and result.empty?
522
+
523
+ result.sort.uniq.select { |f| known_files[f] }
524
+ end
525
+
526
+ ##
527
+ # Sleep then look for files to test, until there are some.
528
+
529
+ def wait_for_changes
530
+ hook :waiting
531
+ Kernel.sleep self.sleep until find_files_to_test
532
+ end
533
+
534
+ ############################################################
535
+ # File Mappings:
536
+
537
+ ##
538
+ # Returns all known files in the codebase matching +regexp+.
539
+
540
+ def files_matching regexp
541
+ self.find_order.select { |k| k =~ regexp }
542
+ end
543
+
544
+ ##
545
+ # Adds a file mapping, optionally prepending the mapping to the
546
+ # front of the list if +prepend+ is true. +regexp+ should match a
547
+ # file path in the codebase. +proc+ is passed a matched filename and
548
+ # Regexp.last_match. +proc+ should return an array of tests to run.
549
+ #
550
+ # For example, if test_helper.rb is modified, rerun all tests:
551
+ #
552
+ # at.add_mapping(/test_helper.rb/) do |f, _|
553
+ # at.files_matching(/^test.*rb$/)
554
+ # end
555
+
556
+ def add_mapping(regexp, prepend = false, &proc)
557
+ if prepend then
558
+ @test_mappings.unshift [regexp, proc]
559
+ else
560
+ @test_mappings.push [regexp, proc]
561
+ end
562
+ nil
563
+ end
564
+
565
+ ##
566
+ # Removed a file mapping matching +regexp+.
567
+
568
+ def remove_mapping regexp
569
+ @test_mappings.delete_if do |k,v|
570
+ k == regexp
571
+ end
572
+ nil
573
+ end
574
+
575
+ ##
576
+ # Clears all file mappings. This is DANGEROUS as it entirely
577
+ # disables autotest. You must add at least one file mapping that
578
+ # does a good job of rerunning appropriate tests.
579
+
580
+ def clear_mappings
581
+ @test_mappings.clear
582
+ nil
583
+ end
584
+
585
+ ############################################################
586
+ # Exceptions:
587
+
588
+ ##
589
+ # Adds +regexp+ to the list of exceptions for find_file. This must
590
+ # be called _before_ the exceptions are compiled.
591
+
592
+ def add_exception regexp
593
+ raise "exceptions already compiled" if defined? @exceptions
594
+ @exception_list << regexp
595
+ nil
596
+ end
597
+
598
+ ##
599
+ # Removes +regexp+ to the list of exceptions for find_file. This
600
+ # must be called _before_ the exceptions are compiled.
601
+
602
+ def remove_exception regexp
603
+ raise "exceptions already compiled" if defined? @exceptions
604
+ @exception_list.delete regexp
605
+ nil
606
+ end
607
+
608
+ ##
609
+ # Clears the list of exceptions for find_file. This must be called
610
+ # _before_ the exceptions are compiled.
611
+
612
+ def clear_exceptions
613
+ raise "exceptions already compiled" if defined? @exceptions
614
+ @exception_list.clear
615
+ nil
616
+ end
617
+
618
+ ##
619
+ # Return a compiled regexp of exceptions for find_files or nil if no
620
+ # filtering should take place. This regexp is generated from
621
+ # +exception_list+.
622
+
623
+ def exceptions
624
+ unless defined? @exceptions then
625
+ if @exception_list.empty? then
626
+ @exceptions = nil
627
+ else
628
+ @exceptions = Regexp.union(*@exception_list)
629
+ end
630
+ end
631
+
632
+ @exceptions
633
+ end
634
+
635
+ ############################################################
636
+ # Hooks:
637
+
638
+ ##
639
+ # Call the event hook named +name+, executing all registered hooks
640
+ # until one returns true. Returns false if no hook handled the
641
+ # event.
642
+
643
+ def hook(name, *args)
644
+ deprecated = {
645
+ # none currently
646
+ }
647
+
648
+ if deprecated[name] and not HOOKS[name].empty? then
649
+ warn "hook #{name} has been deprecated, use #{deprecated[name]}"
650
+ end
651
+
652
+ HOOKS[name].any? do |plugin|
653
+ plugin[self, *args]
654
+ end
655
+ end
656
+
657
+ ##
658
+ # Add the supplied block to the available hooks, with the given
659
+ # name.
660
+
661
+ def self.add_hook(name, &block)
662
+ HOOKS[name] << block
663
+ end
664
+
665
+ private
666
+
667
+ #list of all available rubygem load paths
668
+ def self.rubygem_load_paths
669
+ begin
670
+ require 'rubygems'
671
+ Gem.latest_load_paths
672
+ rescue LoadError
673
+ []
674
+ end
675
+ end
676
+ end