SlimTest 4.6.1.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,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