ryanbriones-ZenTest 3.11.1

Sign up to get free protection for your applications and to get access to all the features.
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