grosser-zentest 4.0.1

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