SlimTest 4.6.1.1

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