ryanbriones-ZenTest 3.11.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.
Files changed (70) hide show
  1. data/History.txt +523 -0
  2. data/Manifest.txt +69 -0
  3. data/README.txt +110 -0
  4. data/Rakefile +68 -0
  5. data/articles/Article.css +721 -0
  6. data/articles/getting_started_with_autotest.html +532 -0
  7. data/articles/how_to_use_zentest.txt +393 -0
  8. data/bin/autotest +55 -0
  9. data/bin/multiruby +40 -0
  10. data/bin/multiruby_setup +68 -0
  11. data/bin/rails_test_audit +80 -0
  12. data/bin/unit_diff +38 -0
  13. data/bin/zentest +28 -0
  14. data/example.txt +42 -0
  15. data/example1.rb +7 -0
  16. data/example2.rb +15 -0
  17. data/example_dot_autotest.rb +45 -0
  18. data/lib/autotest.rb +654 -0
  19. data/lib/autotest/autoupdate.rb +26 -0
  20. data/lib/autotest/camping.rb +37 -0
  21. data/lib/autotest/cctray.rb +57 -0
  22. data/lib/autotest/discover.rb +6 -0
  23. data/lib/autotest/emacs.rb +35 -0
  24. data/lib/autotest/email_notify.rb +66 -0
  25. data/lib/autotest/fixtures.rb +12 -0
  26. data/lib/autotest/growl.rb +28 -0
  27. data/lib/autotest/heckle.rb +14 -0
  28. data/lib/autotest/html_report.rb +31 -0
  29. data/lib/autotest/jabber_notify.rb +111 -0
  30. data/lib/autotest/kdenotify.rb +14 -0
  31. data/lib/autotest/menu.rb +51 -0
  32. data/lib/autotest/migrate.rb +7 -0
  33. data/lib/autotest/notify.rb +34 -0
  34. data/lib/autotest/once.rb +9 -0
  35. data/lib/autotest/pretty.rb +83 -0
  36. data/lib/autotest/rails.rb +81 -0
  37. data/lib/autotest/rcov.rb +22 -0
  38. data/lib/autotest/redgreen.rb +21 -0
  39. data/lib/autotest/restart.rb +11 -0
  40. data/lib/autotest/screen.rb +73 -0
  41. data/lib/autotest/shame.rb +45 -0
  42. data/lib/autotest/snarl.rb +51 -0
  43. data/lib/autotest/timestamp.rb +9 -0
  44. data/lib/functional_test_matrix.rb +92 -0
  45. data/lib/multiruby.rb +401 -0
  46. data/lib/test/rails.rb +295 -0
  47. data/lib/test/rails/controller_test_case.rb +382 -0
  48. data/lib/test/rails/functional_test_case.rb +79 -0
  49. data/lib/test/rails/helper_test_case.rb +64 -0
  50. data/lib/test/rails/ivar_proxy.rb +31 -0
  51. data/lib/test/rails/pp_html_document.rb +74 -0
  52. data/lib/test/rails/rake_tasks.rb +50 -0
  53. data/lib/test/rails/render_tree.rb +93 -0
  54. data/lib/test/rails/test_case.rb +28 -0
  55. data/lib/test/rails/view_test_case.rb +597 -0
  56. data/lib/test/zentest_assertions.rb +134 -0
  57. data/lib/unit_diff.rb +259 -0
  58. data/lib/zentest.rb +566 -0
  59. data/lib/zentest_mapping.rb +99 -0
  60. data/test/test_autotest.rb +449 -0
  61. data/test/test_help.rb +36 -0
  62. data/test/test_rails_autotest.rb +229 -0
  63. data/test/test_rails_controller_test_case.rb +58 -0
  64. data/test/test_rails_helper_test_case.rb +48 -0
  65. data/test/test_rails_view_test_case.rb +275 -0
  66. data/test/test_unit_diff.rb +319 -0
  67. data/test/test_zentest.rb +566 -0
  68. data/test/test_zentest_assertions.rb +128 -0
  69. data/test/test_zentest_mapping.rb +222 -0
  70. metadata +151 -0
@@ -0,0 +1,134 @@
1
+ require 'test/unit/assertions'
2
+
3
+ ##
4
+ # Extra assertions for Test::Unit
5
+
6
+ module Test::Unit::Assertions
7
+ has_miniunit = defined? ::Mini
8
+
9
+ if has_miniunit then
10
+ alias :assert_include :assert_includes
11
+ alias :deny :refute
12
+ alias :deny_empty :refute_empty
13
+ alias :deny_equal :refute_equal
14
+ alias :deny_include :refute_includes
15
+ alias :deny_includes :refute_includes
16
+ alias :deny_nil :refute_nil
17
+ alias :util_capture :capture_io
18
+ else
19
+
20
+ alias :refute_nil :assert_not_nil
21
+
22
+ ##
23
+ # Asserts that +obj+ responds to #empty? and #empty? returns true.
24
+
25
+ def assert_empty(obj)
26
+ assert_respond_to obj, :empty?
27
+ assert_block "#{obj.inspect} expected to be empty." do obj.empty? end
28
+ end
29
+
30
+ ##
31
+ # Like assert_in_delta but better dealing with errors proportional
32
+ # to the sizes of +a+ and +b+.
33
+
34
+ def assert_in_epsilon(a, b, epsilon, message = nil)
35
+ return assert(true) if a == b # count assertion
36
+
37
+ error = ((a - b).to_f / ((b.abs > a.abs) ? b : a)).abs
38
+ message ||= "#{a} expected to be within #{epsilon * 100}% of #{b}, was #{error}"
39
+
40
+ assert_block message do error <= epsilon end
41
+ end
42
+
43
+ ##
44
+ # Asserts that +collection+ includes +obj+.
45
+
46
+ def assert_include collection, obj, msg = nil
47
+ assert_respond_to collection, :include?
48
+
49
+ message ||= "#{collection.inspect}\ndoes not include\n#{obj.inspect}."
50
+ assert_block message do collection.include?(obj) end
51
+ end
52
+
53
+ alias assert_includes assert_include
54
+
55
+ ##
56
+ # Asserts that +boolean+ is not false or nil.
57
+
58
+ def deny(boolean, message = nil)
59
+ _wrap_assertion do
60
+ assert_block(build_message(message, "Failed refutation, no message given.")) { not boolean }
61
+ end
62
+ end
63
+
64
+ ##
65
+ # Asserts that +obj+ responds to #empty? and #empty? returns false.
66
+
67
+ def deny_empty(obj)
68
+ assert_respond_to obj, :empty?
69
+ assert_block "#{obj.inspect} expected to have stuff." do !obj.empty? end
70
+ end
71
+
72
+ ##
73
+ # Alias for assert_not_equal
74
+
75
+ alias deny_equal assert_not_equal # rescue nil # rescue for miniunit
76
+
77
+ ##
78
+ # Asserts that +obj+ responds to #include? and that obj does not include
79
+ # +item+.
80
+
81
+ def deny_include(collection, obj, message = nil)
82
+ assert_respond_to collection, :include?
83
+ message ||= "#{collection.inspect} includes #{obj.inspect}."
84
+ assert_block message do !collection.include? obj end
85
+ end
86
+
87
+ alias deny_includes deny_include
88
+
89
+ ##
90
+ # Asserts that +obj+ is not nil.
91
+
92
+ alias deny_nil assert_not_nil
93
+
94
+ ##
95
+ # Captures $stdout and $stderr to StringIO objects and returns them.
96
+ # Restores $stdout and $stderr when done.
97
+ #
98
+ # Usage:
99
+ # def test_puts
100
+ # out, err = capture do
101
+ # puts 'hi'
102
+ # STDERR.puts 'bye!'
103
+ # end
104
+ # assert_equal "hi\n", out.string
105
+ # assert_equal "bye!\n", err.string
106
+ # end
107
+
108
+ def util_capture
109
+ require 'stringio'
110
+ orig_stdout = $stdout.dup
111
+ orig_stderr = $stderr.dup
112
+ captured_stdout = StringIO.new
113
+ captured_stderr = StringIO.new
114
+ $stdout = captured_stdout
115
+ $stderr = captured_stderr
116
+ yield
117
+ captured_stdout.rewind
118
+ captured_stderr.rewind
119
+ return captured_stdout.string, captured_stderr.string
120
+ ensure
121
+ $stdout = orig_stdout
122
+ $stderr = orig_stderr
123
+ end
124
+ end
125
+ end
126
+
127
+ class Object # :nodoc:
128
+ unless respond_to? :path2class then
129
+ def path2class(path) # :nodoc:
130
+ path.split('::').inject(Object) { |k,n| k.const_get n }
131
+ end
132
+ end
133
+ end
134
+
data/lib/unit_diff.rb ADDED
@@ -0,0 +1,259 @@
1
+ require 'tempfile'
2
+
3
+ ##
4
+ # UnitDiff makes reading Test::Unit output easy and fun. Instead of a
5
+ # confusing jumble of text with nearly unnoticable changes like this:
6
+ #
7
+ # 1) Failure:
8
+ # test_to_gpoints(RouteTest) [test/unit/route_test.rb:29]:
9
+ # <"new GPolyline([\n new GPoint( 47.00000, -122.00000),\n new GPoint( 46.5000
10
+ # 0, -122.50000),\n new GPoint( 46.75000, -122.75000),\n new GPoint( 46.00000,
11
+ # -123.00000)])"> expected but was
12
+ # <"new Gpolyline([\n new GPoint( 47.00000, -122.00000),\n new GPoint( 46.5000
13
+ # 0, -122.50000),\n new GPoint( 46.75000, -122.75000),\n new GPoint( 46.00000,
14
+ # -123.00000)])">.
15
+ #
16
+ #
17
+ # You get an easy-to-read diff output like this:
18
+ #
19
+ # 1) Failure:
20
+ # test_to_gpoints(RouteTest) [test/unit/route_test.rb:29]:
21
+ # 1c1
22
+ # < new GPolyline([
23
+ # ---
24
+ # > new Gpolyline([
25
+ #
26
+ # == Usage
27
+ #
28
+ # test.rb | unit_diff [options]
29
+ # options:
30
+ # -b ignore whitespace differences
31
+ # -c contextual diff
32
+ # -h show usage
33
+ # -k keep temp diff files around
34
+ # -l prefix line numbers on the diffs
35
+ # -u unified diff
36
+ # -v display version
37
+
38
+ class UnitDiff
39
+
40
+ WINDOZE = /win32/ =~ RUBY_PLATFORM unless defined? WINDOZE
41
+ DIFF = if WINDOZE
42
+ 'diff.exe'
43
+ else
44
+ if system("gdiff", __FILE__, __FILE__)
45
+ 'gdiff' # solaris and kin suck
46
+ else
47
+ 'diff'
48
+ end
49
+ end unless defined? DIFF
50
+
51
+ ##
52
+ # Handy wrapper for UnitDiff#unit_diff.
53
+
54
+ def self.unit_diff
55
+ trap 'INT' do exit 1 end
56
+ puts UnitDiff.new.unit_diff
57
+ end
58
+
59
+ def parse_input(input, output)
60
+ current = []
61
+ data = []
62
+ data << current
63
+ print_lines = true
64
+
65
+ term = "\nFinished".split(//).map { |c| c[0] }
66
+ term_length = term.size
67
+
68
+ old_sync = output.sync
69
+ output.sync = true
70
+ while line = input.gets
71
+ case line
72
+ when /^(Loaded suite|Started)/ then
73
+ print_lines = true
74
+ output.puts line
75
+ chars = []
76
+ while c = input.getc do
77
+ output.putc c
78
+ chars << c
79
+ tail = chars[-term_length..-1]
80
+ break if chars.size >= term_length and tail == term
81
+ end
82
+ output.puts input.gets # the rest of "Finished in..."
83
+ output.puts
84
+ next
85
+ when /^\s*$/, /^\(?\s*\d+\) (Failure|Error):/, /^\d+\)/ then
86
+ print_lines = false
87
+ current = []
88
+ data << current
89
+ when /^Finished in \d/ then
90
+ print_lines = false
91
+ end
92
+ output.puts line if print_lines
93
+ current << line
94
+ end
95
+ output.sync = old_sync
96
+ data = data.reject { |o| o == ["\n"] or o.empty? }
97
+ footer = data.pop
98
+
99
+ return data, footer
100
+ end
101
+
102
+ # Parses a single diff recording the header and what
103
+ # was expected, and what was actually obtained.
104
+ def parse_diff(result)
105
+ header = []
106
+ expect = []
107
+ butwas = []
108
+ footer = []
109
+ found = false
110
+ state = :header
111
+
112
+ until result.empty? do
113
+ case state
114
+ when :header then
115
+ header << result.shift
116
+ state = :expect if result.first =~ /^<|^Expected/
117
+ when :expect then
118
+ case result.first
119
+ when /^Expected (.*?) to equal (.*?):$/ then
120
+ expect << $1
121
+ butwas << $2
122
+ state = :footer
123
+ result.shift
124
+ when /^Expected (.*?), not (.*)$/m then
125
+ expect << $1
126
+ butwas << $2
127
+ state = :footer
128
+ result.shift
129
+ when /^Expected (.*?)$/ then
130
+ expect << "#{$1}\n"
131
+ result.shift
132
+ when /^to equal / then
133
+ state = :spec_butwas
134
+ bw = result.shift.sub(/^to equal (.*):?$/, '\1')
135
+ butwas << bw
136
+ else
137
+ state = :butwas if result.first.sub!(/ expected( but was|, not)/, '')
138
+ expect << result.shift
139
+ end
140
+ when :butwas then
141
+ butwas = result[0..-1]
142
+ result.clear
143
+ when :spec_butwas then
144
+ if result.first =~ /^\s+\S+ at |^:\s*$/
145
+ state = :footer
146
+ else
147
+ butwas << result.shift
148
+ end
149
+ when :footer then
150
+ butwas.last.sub!(/:$/, '')
151
+ footer = result.map {|l| l.chomp }
152
+ result.clear
153
+ else
154
+ raise "unknown state #{state}"
155
+ end
156
+ end
157
+
158
+ return header, expect, nil, footer if butwas.empty?
159
+
160
+ expect.last.chomp!
161
+ expect.first.sub!(/^<\"/, '')
162
+ expect.last.sub!(/\">$/, '')
163
+
164
+ butwas.last.chomp!
165
+ butwas.last.chop! if butwas.last =~ /\.$/
166
+ butwas.first.sub!( /^<\"/, '')
167
+ butwas.last.sub!(/\">$/, '')
168
+
169
+ return header, expect, butwas, footer
170
+ end
171
+
172
+ ##
173
+ # Scans Test::Unit output +input+ looking for comparison failures and makes
174
+ # them easily readable by passing them through diff.
175
+
176
+ def unit_diff(input=ARGF, output=$stdout)
177
+ $b = false unless defined? $b
178
+ $c = false unless defined? $c
179
+ $k = false unless defined? $k
180
+ $l = false unless defined? $l
181
+ $u = false unless defined? $u
182
+
183
+ data, footer = self.parse_input(input, output)
184
+
185
+ output = []
186
+
187
+ # Output
188
+ data.each do |result|
189
+ first = []
190
+ second = []
191
+
192
+ if result.first =~ /Error/ then
193
+ output.push result.join('')
194
+ next
195
+ end
196
+
197
+ prefix, expect, butwas, result_footer = parse_diff(result)
198
+
199
+ output.push prefix.compact.map {|line| line.strip}.join("\n")
200
+
201
+ if butwas then
202
+ output.push self.diff(expect, butwas)
203
+
204
+ output.push result_footer
205
+ output.push ''
206
+ else
207
+ output.push expect.join('')
208
+ end
209
+ end
210
+
211
+ if footer then
212
+ footer.shift if footer.first.strip.empty?# unless footer.first.nil?
213
+ output.push footer.compact.map {|line| line.strip}.join("\n")
214
+ end
215
+
216
+ return output.flatten.join("\n")
217
+ end
218
+
219
+ def diff expect, butwas
220
+ output = nil
221
+
222
+ Tempfile.open("expect") do |a|
223
+ a.write(massage(expect))
224
+ a.rewind
225
+ Tempfile.open("butwas") do |b|
226
+ b.write(massage(butwas))
227
+ b.rewind
228
+
229
+ diff_flags = $u ? "-u" : $c ? "-c" : ""
230
+ diff_flags += " -b" if $b
231
+
232
+ result = `#{DIFF} #{diff_flags} #{a.path} #{b.path}`
233
+ output = if result.empty? then
234
+ "[no difference--suspect ==]"
235
+ else
236
+ result.split(/\n/)
237
+ end
238
+
239
+ if $k then
240
+ warn "moving #{a.path} to #{a.path}.keep"
241
+ File.rename a.path, a.path + ".keep"
242
+ warn "moving #{b.path} to #{b.path}.keep"
243
+ File.rename b.path, b.path + ".keep"
244
+ end
245
+ end
246
+ end
247
+
248
+ output
249
+ end
250
+
251
+ def massage(data)
252
+ data = data.map { |l| '%3d) %s' % [count+=1, l] } if $l
253
+ # unescape newlines, strip <> from entire string
254
+ data = data.join
255
+ data = data.gsub(/\\n/, "\n").gsub(/0x[a-f0-9]+/m, '0xXXXXXX') + "\n"
256
+ data += "\n" unless data[-1] == ?\n
257
+ data
258
+ end
259
+ end
data/lib/zentest.rb ADDED
@@ -0,0 +1,566 @@
1
+
2
+ require 'zentest_mapping'
3
+
4
+ $stdlib = {}
5
+ ObjectSpace.each_object(Module) { |m| $stdlib[m.name] = true }
6
+
7
+ $:.unshift( *$I.split(/:/) ) if defined? $I and String === $I
8
+ $r = false unless defined? $r # reverse mapping for testclass names
9
+
10
+ if $r then
11
+ # all this is needed because rails is retarded
12
+ $-w = false
13
+ $: << 'test'
14
+ $: << 'lib'
15
+ require 'config/environment'
16
+ f = './app/controllers/application.rb'
17
+ require f if test ?f, f
18
+ end
19
+
20
+ $ZENTEST = true
21
+ $TESTING = true
22
+
23
+ require 'test/unit/testcase' # helps required modules
24
+
25
+ class Module
26
+
27
+ def zentest
28
+ at_exit { ZenTest.autotest(self) }
29
+ end
30
+
31
+ end
32
+
33
+ ##
34
+ # ZenTest scans your target and unit-test code and writes your missing
35
+ # code based on simple naming rules, enabling XP at a much quicker
36
+ # pace. ZenTest only works with Ruby and Test::Unit.
37
+ #
38
+ # == RULES
39
+ #
40
+ # ZenTest uses the following rules to figure out what code should be
41
+ # generated:
42
+ #
43
+ # * Definition:
44
+ # * CUT = Class Under Test
45
+ # * TC = Test Class (for CUT)
46
+ # * TC's name is the same as CUT w/ "Test" prepended at every scope level.
47
+ # * Example: TestA::TestB vs A::B.
48
+ # * CUT method names are used in CT, with "test_" prependend and optional "_ext" extensions for differentiating test case edge boundaries.
49
+ # * Example:
50
+ # * A::B#blah
51
+ # * TestA::TestB#test_blah_normal
52
+ # * TestA::TestB#test_blah_missing_file
53
+ # * All naming conventions are bidirectional with the exception of test extensions.
54
+ #
55
+ # See ZenTestMapping for documentation on method naming.
56
+
57
+ class ZenTest
58
+
59
+ VERSION = '3.11.1'
60
+
61
+ include ZenTestMapping
62
+
63
+ if $TESTING then
64
+ attr_reader :missing_methods
65
+ attr_accessor :test_klasses
66
+ attr_accessor :klasses
67
+ attr_accessor :inherited_methods
68
+ else
69
+ def missing_methods; raise "Something is wack"; end
70
+ end
71
+
72
+ def initialize
73
+ @result = []
74
+ @test_klasses = {}
75
+ @klasses = {}
76
+ @error_count = 0
77
+ @inherited_methods = Hash.new { |h,k| h[k] = {} }
78
+ # key = klassname, val = hash of methods => true
79
+ @missing_methods = Hash.new { |h,k| h[k] = {} }
80
+ end
81
+
82
+ # load_file wraps require, skipping the loading of $0.
83
+ def load_file(file)
84
+ puts "# loading #{file} // #{$0}" if $DEBUG
85
+
86
+ unless file == $0 then
87
+ begin
88
+ require file
89
+ rescue LoadError => err
90
+ puts "Could not load #{file}: #{err}"
91
+ end
92
+ else
93
+ puts "# Skipping loading myself (#{file})" if $DEBUG
94
+ end
95
+ end
96
+
97
+ # obtain the class klassname, either from Module or
98
+ # using ObjectSpace to search for it.
99
+ def get_class(klassname)
100
+ begin
101
+ klass = Module.const_get(klassname.intern)
102
+ puts "# found class #{klass.name}" if $DEBUG
103
+ rescue NameError
104
+ ObjectSpace.each_object(Class) do |cls|
105
+ if cls.name =~ /(^|::)#{klassname}$/ then
106
+ klass = cls
107
+ klassname = cls.name
108
+ break
109
+ end
110
+ end
111
+ puts "# searched and found #{klass.name}" if klass and $DEBUG
112
+ end
113
+
114
+ if klass.nil? and not $TESTING then
115
+ puts "Could not figure out how to get #{klassname}..."
116
+ puts "Report to support-zentest@zenspider.com w/ relevant source"
117
+ end
118
+
119
+ return klass
120
+ end
121
+
122
+ # Get the public instance, class and singleton methods for
123
+ # class klass. If full is true, include the methods from
124
+ # Kernel and other modules that get included. The methods
125
+ # suite, new, pretty_print, pretty_print_cycle will not
126
+ # be included in the resuting array.
127
+ def get_methods_for(klass, full=false)
128
+ klass = self.get_class(klass) if klass.kind_of? String
129
+
130
+ # WTF? public_instance_methods: default vs true vs false = 3 answers
131
+ # to_s on all results if ruby >= 1.9
132
+ public_methods = klass.public_instance_methods(false)
133
+ public_methods -= Kernel.methods unless full
134
+ public_methods.map! { |m| m.to_s }
135
+ public_methods -= %w(pretty_print pretty_print_cycle)
136
+
137
+ klass_methods = klass.singleton_methods(full)
138
+ klass_methods -= Class.public_methods(true)
139
+ klass_methods = klass_methods.map { |m| "self.#{m}" }
140
+ klass_methods -= %w(self.suite new)
141
+
142
+ result = {}
143
+ (public_methods + klass_methods).each do |meth|
144
+ puts "# found method #{meth}" if $DEBUG
145
+ result[meth] = true
146
+ end
147
+
148
+ return result
149
+ end
150
+
151
+ # Return the methods for class klass, as a hash with the
152
+ # method nemas as keys, and true as the value for all keys.
153
+ # Unless full is true, leave out the methods for Object which
154
+ # all classes get.
155
+ def get_inherited_methods_for(klass, full)
156
+ klass = self.get_class(klass) if klass.kind_of? String
157
+
158
+ klassmethods = {}
159
+ if (klass.class.method_defined?(:superclass)) then
160
+ superklass = klass.superclass
161
+ if superklass then
162
+ the_methods = superklass.instance_methods(true)
163
+
164
+ # generally we don't test Object's methods...
165
+ unless full then
166
+ the_methods -= Object.instance_methods(true)
167
+ the_methods -= Kernel.methods # FIX (true) - check 1.6 vs 1.8
168
+ end
169
+
170
+ the_methods.each do |meth|
171
+ klassmethods[meth.to_s] = true
172
+ end
173
+ end
174
+ end
175
+ return klassmethods
176
+ end
177
+
178
+ # Check the class klass is a testing class
179
+ # (by inspecting its name).
180
+ def is_test_class(klass)
181
+ klass = klass.to_s
182
+ klasspath = klass.split(/::/)
183
+ a_bad_classpath = klasspath.find do |s| s !~ ($r ? /Test$/ : /^Test/) end
184
+ return a_bad_classpath.nil?
185
+ end
186
+
187
+ # Generate the name of a testclass from non-test class
188
+ # so that Foo::Blah => TestFoo::TestBlah, etc. It the
189
+ # name is already a test class, convert it the other way.
190
+ def convert_class_name(name)
191
+ name = name.to_s
192
+
193
+ if self.is_test_class(name) then
194
+ if $r then
195
+ name = name.gsub(/Test($|::)/, '\1') # FooTest::BlahTest => Foo::Blah
196
+ else
197
+ name = name.gsub(/(^|::)Test/, '\1') # TestFoo::TestBlah => Foo::Blah
198
+ end
199
+ else
200
+ if $r then
201
+ name = name.gsub(/($|::)/, 'Test\1') # Foo::Blah => FooTest::BlahTest
202
+ else
203
+ name = name.gsub(/(^|::)/, '\1Test') # Foo::Blah => TestFoo::TestBlah
204
+ end
205
+ end
206
+
207
+ return name
208
+ end
209
+
210
+ # Does all the work of finding a class by name,
211
+ # obtaining its methods and those of its superclass.
212
+ # The full parameter determines if all the methods
213
+ # including those of Object and mixed in modules
214
+ # are obtained (true if they are, false by default).
215
+ def process_class(klassname, full=false)
216
+ klass = self.get_class(klassname)
217
+ raise "Couldn't get class for #{klassname}" if klass.nil?
218
+ klassname = klass.name # refetch to get full name
219
+
220
+ is_test_class = self.is_test_class(klassname)
221
+ target = is_test_class ? @test_klasses : @klasses
222
+
223
+ # record public instance methods JUST in this class
224
+ target[klassname] = self.get_methods_for(klass, full)
225
+
226
+ # record ALL instance methods including superclasses (minus Object)
227
+ # Only minus Object if full is true.
228
+ @inherited_methods[klassname] = self.get_inherited_methods_for(klass, full)
229
+ return klassname
230
+ end
231
+
232
+ # Work through files, collecting class names, method names
233
+ # and assertions. Detects ZenTest (SKIP|FULL) comments
234
+ # in the bodies of classes.
235
+ # For each class a count of methods and test methods is
236
+ # kept, and the ratio noted.
237
+ def scan_files(*files)
238
+ assert_count = Hash.new(0)
239
+ method_count = Hash.new(0)
240
+ klassname = nil
241
+
242
+ files.each do |path|
243
+ is_loaded = false
244
+
245
+ # if reading stdin, slurp the whole thing at once
246
+ file = (path == "-" ? $stdin.read : File.new(path))
247
+
248
+ file.each_line do |line|
249
+
250
+ if klassname then
251
+ case line
252
+ when /^\s*def/ then
253
+ method_count[klassname] += 1
254
+ when /assert|flunk/ then
255
+ assert_count[klassname] += 1
256
+ end
257
+ end
258
+
259
+ if line =~ /^\s*(?:class|module)\s+([\w:]+)/ then
260
+ klassname = $1
261
+
262
+ if line =~ /\#\s*ZenTest SKIP/ then
263
+ klassname = nil
264
+ next
265
+ end
266
+
267
+ full = false
268
+ if line =~ /\#\s*ZenTest FULL/ then
269
+ full = true
270
+ end
271
+
272
+ unless is_loaded then
273
+ unless path == "-" then
274
+ self.load_file(path)
275
+ else
276
+ eval file, TOPLEVEL_BINDING
277
+ end
278
+ is_loaded = true
279
+ end
280
+
281
+ begin
282
+ klassname = self.process_class(klassname, full)
283
+ rescue
284
+ puts "# Couldn't find class for name #{klassname}"
285
+ next
286
+ end
287
+
288
+ # Special Case: ZenTest is already loaded since we are running it
289
+ if klassname == "TestZenTest" then
290
+ klassname = "ZenTest"
291
+ self.process_class(klassname, false)
292
+ end
293
+
294
+ end # if /class/
295
+ end # IO.foreach
296
+ end # files
297
+
298
+ result = []
299
+ method_count.each_key do |classname|
300
+
301
+ entry = {}
302
+
303
+ next if is_test_class(classname)
304
+ testclassname = convert_class_name(classname)
305
+ a_count = assert_count[testclassname]
306
+ m_count = method_count[classname]
307
+ ratio = a_count.to_f / m_count.to_f * 100.0
308
+
309
+ entry['n'] = classname
310
+ entry['r'] = ratio
311
+ entry['a'] = a_count
312
+ entry['m'] = m_count
313
+
314
+ result.push entry
315
+ end
316
+
317
+ sorted_results = result.sort { |a,b| b['r'] <=> a['r'] }
318
+
319
+ @result.push sprintf("# %25s: %4s / %4s = %6s%%", "classname", "asrt", "meth", "ratio")
320
+ sorted_results.each do |e|
321
+ @result.push sprintf("# %25s: %4d / %4d = %6.2f%%", e['n'], e['a'], e['m'], e['r'])
322
+ end
323
+ end
324
+
325
+ # Adds a missing method to the collected results.
326
+ def add_missing_method(klassname, methodname)
327
+ @result.push "# ERROR method #{klassname}\##{methodname} does not exist (1)" if $DEBUG and not $TESTING
328
+ @error_count += 1
329
+ @missing_methods[klassname][methodname] = true
330
+ end
331
+
332
+ # looks up the methods and the corresponding test methods
333
+ # in the collection already built. To reduce duplication
334
+ # and hide implementation details.
335
+ def methods_and_tests(klassname, testklassname)
336
+ return @klasses[klassname], @test_klasses[testklassname]
337
+ end
338
+
339
+ # Checks, for the given class klassname, that each method
340
+ # has a corrsponding test method. If it doesn't this is
341
+ # added to the information for that class
342
+ def analyze_impl(klassname)
343
+ testklassname = self.convert_class_name(klassname)
344
+ if @test_klasses[testklassname] then
345
+ methods, testmethods = methods_and_tests(klassname,testklassname)
346
+
347
+ # check that each method has a test method
348
+ @klasses[klassname].each_key do | methodname |
349
+ testmethodname = normal_to_test(methodname)
350
+ unless testmethods[testmethodname] then
351
+ begin
352
+ unless testmethods.keys.find { |m| m =~ /#{testmethodname}(_\w+)+$/ } then
353
+ self.add_missing_method(testklassname, testmethodname)
354
+ end
355
+ rescue RegexpError => e
356
+ puts "# ERROR trying to use '#{testmethodname}' as a regex. Look at #{klassname}.#{methodname}"
357
+ end
358
+ end # testmethods[testmethodname]
359
+ end # @klasses[klassname].each_key
360
+ else # ! @test_klasses[testklassname]
361
+ puts "# ERROR test class #{testklassname} does not exist" if $DEBUG
362
+ @error_count += 1
363
+
364
+ @klasses[klassname].keys.each do | methodname |
365
+ self.add_missing_method(testklassname, normal_to_test(methodname))
366
+ end
367
+ end # @test_klasses[testklassname]
368
+ end
369
+
370
+ # For the given test class testklassname, ensure that all
371
+ # the test methods have corresponding (normal) methods.
372
+ # If not, add them to the information about that class.
373
+ def analyze_test(testklassname)
374
+ klassname = self.convert_class_name(testklassname)
375
+
376
+ # CUT might be against a core class, if so, slurp it and analyze it
377
+ if $stdlib[klassname] then
378
+ self.process_class(klassname, true)
379
+ self.analyze_impl(klassname)
380
+ end
381
+
382
+ if @klasses[klassname] then
383
+ methods, testmethods = methods_and_tests(klassname,testklassname)
384
+
385
+ # check that each test method has a method
386
+ testmethods.each_key do | testmethodname |
387
+ if testmethodname =~ /^test_(?!integration_)/ then
388
+
389
+ # try the current name
390
+ methodname = test_to_normal(testmethodname, klassname)
391
+ orig_name = methodname.dup
392
+
393
+ found = false
394
+ until methodname == "" or methods[methodname] or @inherited_methods[klassname][methodname] do
395
+ # try the name minus an option (ie mut_opt1 -> mut)
396
+ if methodname.sub!(/_[^_]+$/, '') then
397
+ if methods[methodname] or @inherited_methods[klassname][methodname] then
398
+ found = true
399
+ end
400
+ else
401
+ break # no more substitutions will take place
402
+ end
403
+ end # methodname == "" or ...
404
+
405
+ unless found or methods[methodname] or methodname == "initialize" then
406
+ self.add_missing_method(klassname, orig_name)
407
+ end
408
+
409
+ else # not a test_.* method
410
+ unless testmethodname =~ /^util_/ then
411
+ puts "# WARNING Skipping #{testklassname}\##{testmethodname}" if $DEBUG
412
+ end
413
+ end # testmethodname =~ ...
414
+ end # testmethods.each_key
415
+ else # ! @klasses[klassname]
416
+ puts "# ERROR class #{klassname} does not exist" if $DEBUG
417
+ @error_count += 1
418
+
419
+ @test_klasses[testklassname].keys.each do |testmethodname|
420
+ @missing_methods[klassname][test_to_normal(testmethodname)] = true
421
+ end
422
+ end # @klasses[klassname]
423
+ end
424
+
425
+ # create a given method at a given
426
+ # indentation. Returns an array containing
427
+ # the lines of the method.
428
+ def create_method(indentunit, indent, name)
429
+ meth = []
430
+ meth.push indentunit*indent + "def #{name}"
431
+ meth.last << "(*args)" unless name =~ /^test/
432
+ indent += 1
433
+ meth.push indentunit*indent + "raise NotImplementedError, 'Need to write #{name}'"
434
+ indent -= 1
435
+ meth.push indentunit*indent + "end"
436
+ return meth
437
+ end
438
+
439
+ # Walk each known class and test that each method has
440
+ # a test method
441
+ # Then do it in the other direction...
442
+ def analyze
443
+ # walk each known class and test that each method has a test method
444
+ @klasses.each_key do |klassname|
445
+ self.analyze_impl(klassname)
446
+ end
447
+
448
+ # now do it in the other direction...
449
+ @test_klasses.each_key do |testklassname|
450
+ self.analyze_test(testklassname)
451
+ end
452
+ end
453
+
454
+ # Using the results gathered during analysis
455
+ # generate skeletal code with methods raising
456
+ # NotImplementedError, so that they can be filled
457
+ # in later, and so the tests will fail to start with.
458
+ def generate_code
459
+
460
+ # @result.unshift "# run against: #{files.join(', ')}" if $DEBUG
461
+ @result.unshift "# Code Generated by ZenTest v. #{VERSION}"
462
+
463
+ if $DEBUG then
464
+ @result.push "# found classes: #{@klasses.keys.join(', ')}"
465
+ @result.push "# found test classes: #{@test_klasses.keys.join(', ')}"
466
+ end
467
+
468
+ if @missing_methods.size > 0 then
469
+ @result.push ""
470
+ @result.push "require 'test/unit' unless defined? $ZENTEST and $ZENTEST"
471
+ @result.push ""
472
+ end
473
+
474
+ indentunit = " "
475
+
476
+ @missing_methods.keys.sort.each do |fullklasspath|
477
+
478
+ methods = @missing_methods[fullklasspath]
479
+ cls_methods = methods.keys.grep(/^(self\.|test_class_)/)
480
+ methods.delete_if {|k,v| cls_methods.include? k }
481
+
482
+ next if methods.empty? and cls_methods.empty?
483
+
484
+ indent = 0
485
+ is_test_class = self.is_test_class(fullklasspath)
486
+ klasspath = fullklasspath.split(/::/)
487
+ klassname = klasspath.pop
488
+
489
+ klasspath.each do | modulename |
490
+ m = self.get_class(modulename)
491
+ type = m.nil? ? "module" : m.class.name.downcase
492
+ @result.push indentunit*indent + "#{type} #{modulename}"
493
+ indent += 1
494
+ end
495
+ @result.push indentunit*indent + "class #{klassname}" + (is_test_class ? " < Test::Unit::TestCase" : '')
496
+ indent += 1
497
+
498
+ meths = []
499
+
500
+ cls_methods.sort.each do |method|
501
+ meth = create_method(indentunit, indent, method)
502
+ meths.push meth.join("\n")
503
+ end
504
+
505
+ methods.keys.sort.each do |method|
506
+ next if method =~ /pretty_print/
507
+ meth = create_method(indentunit, indent, method)
508
+ meths.push meth.join("\n")
509
+ end
510
+
511
+ @result.push meths.join("\n\n")
512
+
513
+ indent -= 1
514
+ @result.push indentunit*indent + "end"
515
+ klasspath.each do | modulename |
516
+ indent -= 1
517
+ @result.push indentunit*indent + "end"
518
+ end
519
+ @result.push ''
520
+ end
521
+
522
+ @result.push "# Number of errors detected: #{@error_count}"
523
+ @result.push ''
524
+ end
525
+
526
+ # presents results in a readable manner.
527
+ def result
528
+ return @result.join("\n")
529
+ end
530
+
531
+ # Runs ZenTest over all the supplied files so that
532
+ # they are analysed and the missing methods have
533
+ # skeleton code written.
534
+ def self.fix(*files)
535
+ zentest = ZenTest.new
536
+ zentest.scan_files(*files)
537
+ zentest.analyze
538
+ zentest.generate_code
539
+ return zentest.result
540
+ end
541
+
542
+ # Process all the supplied classes for methods etc,
543
+ # and analyse the results. Generate the skeletal code
544
+ # and eval it to put the methods into the runtime
545
+ # environment.
546
+ def self.autotest(*klasses)
547
+ zentest = ZenTest.new
548
+ klasses.each do |klass|
549
+ zentest.process_class(klass)
550
+ end
551
+
552
+ zentest.analyze
553
+
554
+ zentest.missing_methods.each do |klass,methods|
555
+ methods.each do |method,x|
556
+ warn "autotest generating #{klass}##{method}"
557
+ end
558
+ end
559
+
560
+ zentest.generate_code
561
+ code = zentest.result
562
+ puts code if $DEBUG
563
+
564
+ Object.class_eval code
565
+ end
566
+ end