zentest-without-autotest 4.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/multiruby.rb ADDED
@@ -0,0 +1,426 @@
1
+ require 'fileutils'
2
+ require 'open-uri'
3
+
4
+ ##
5
+ # multiruby_setup is a script to help you manage multiruby.
6
+ #
7
+ # usage: multiruby_setup [-h|cmd|spec...]
8
+ #
9
+ # cmds:
10
+ #
11
+ # -h, --help, help = show this help.
12
+ # build = build and install everything. used internally.
13
+ # clean = clean scm build dirs and remove non-scm build dirs.
14
+ # list = print installed versions.
15
+ # rm:$version = remove a particular version.
16
+ # rubygems:merge = symlink all rubygem dirs to one dir.
17
+ # tags = list all tags from svn.
18
+ # update = update svn builds.
19
+ # update:rubygems = update rubygems and nuke install dirs.
20
+ #
21
+ # specs:
22
+ #
23
+ # the_usual = alias for latest versions from tar + rubygems
24
+ # mri:svn:current = alias for mri:svn:releases and mri:svn:branches.
25
+ # mri:svn:releases = alias for supported releases of mri ruby.
26
+ # mri:svn:branches = alias for active branches of mri ruby.
27
+ # mri:svn:branch:$branch = install a specific $branch of mri from svn.
28
+ # mri:svn:tag:$tag = install a specific $tag of mri from svn.
29
+ # mri:tar:$version = install a specific $version of mri from tarball.
30
+ # rbx:ln:$dir = symlink your rbx $dir
31
+ # rbx:git:current = install rbx from git
32
+ #
33
+ # environment variables:
34
+ #
35
+ # GEM_URL = url for rubygems tarballs
36
+ # MRI_SVN = url for MRI SVN
37
+ # RBX_GIT = url for rubinius git
38
+ # RUBY_URL = url for MRI tarballs
39
+ # VERSIONS = what versions to install
40
+ #
41
+ # RUBYOPT is cleared on installs.
42
+ #
43
+ # NOTES:
44
+ #
45
+ # * you can add a symlink to your rubinius build into ~/.multiruby/install
46
+ # * I need patches/maintainers for other implementations.
47
+ #
48
+ module Multiruby
49
+ def self.env name, fallback; ENV[name] || fallback; end # :nodoc:
50
+
51
+ TAGS = %w( 1_8_6 1_8_7 1_9_1)
52
+ BRANCHES = %w(1_8 1_8_6 1_8_7 trunk)
53
+
54
+ VERSIONS = env('VERSIONS', TAGS.join(":").gsub(/_/, '.')).split(/:/)
55
+ MRI_SVN = env 'MRI_SVN', 'http://svn.ruby-lang.org/repos/ruby'
56
+ RBX_GIT = env 'RBX_GIT', 'git://github.com/evanphx/rubinius.git'
57
+ RUBY_URL = env 'RUBY_URL', 'http://ftp.ruby-lang.org/pub/ruby'
58
+ GEM_URL = env 'GEM_URL', 'http://files.rubyforge.vm.bytemark.co.uk/rubygems'
59
+
60
+ HELP = []
61
+
62
+ File.readlines(__FILE__).each do |line|
63
+ next unless line =~ /^#( |$)/
64
+ HELP << line.sub(/^# ?/, '')
65
+ end
66
+
67
+ def self.build_and_install
68
+ ENV.delete 'RUBYOPT'
69
+
70
+ root_dir = self.root_dir
71
+ versions = []
72
+
73
+ Dir.chdir root_dir do
74
+ self.setup_dirs
75
+
76
+ rubygems = Dir["versions/rubygems*.tgz"]
77
+ abort "You should delete all but one rubygem tarball" if rubygems.size > 1
78
+ rubygem_tarball = File.expand_path rubygems.last rescue nil
79
+
80
+ Dir.chdir "build" do
81
+ Dir["../versions/*"].sort.each do |tarball|
82
+ next if tarball =~ /rubygems/
83
+
84
+ build_dir = File.basename tarball, ".tar.gz"
85
+ version = build_dir.sub(/^ruby-?/, '')
86
+ versions << version
87
+ inst_dir = "#{root_dir}/install/#{version}"
88
+
89
+ unless test ?d, inst_dir then
90
+ unless test ?d, build_dir then
91
+ if test ?d, tarball then
92
+ dir = File.basename tarball
93
+ FileUtils.ln_sf "../versions/#{dir}", "../build/#{dir}"
94
+ else
95
+ puts "creating #{inst_dir}"
96
+ Dir.mkdir inst_dir
97
+ run "tar zxf #{tarball}"
98
+ end
99
+ end
100
+ Dir.chdir build_dir do
101
+ puts "building and installing #{version}"
102
+ if test ?f, "configure.in" then
103
+ gnu_utils_build inst_dir
104
+ elsif test ?f, "Rakefile" then
105
+ rake_build inst_dir
106
+ else
107
+ raise "dunno how to build"
108
+ end
109
+
110
+ if rubygem_tarball and version !~ /1[._-]9|mri_trunk|rubinius/ then
111
+ rubygems = File.basename rubygem_tarball, ".tgz"
112
+ run "tar zxf #{rubygem_tarball}" unless test ?d, rubygems
113
+
114
+ Dir.chdir rubygems do
115
+ run "../ruby ./setup.rb --no-rdoc --no-ri", "../log.rubygems"
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ versions
125
+ end
126
+
127
+ def self.clean
128
+ self.each_scm_build_dir do |style|
129
+ case style
130
+ when :svn, :git then
131
+ if File.exist? "Rakefile" then
132
+ run "rake clean"
133
+ elsif File.exist? "Makefile" then
134
+ run "make clean"
135
+ end
136
+ else
137
+ FileUtils.rm_rf Dir.pwd
138
+ end
139
+ end
140
+ end
141
+
142
+ def self.each_scm_build_dir
143
+ Multiruby.in_build_dir do
144
+ Dir["*"].each do |dir|
145
+ next unless File.directory? dir
146
+ Dir.chdir dir do
147
+ if File.exist?(".svn") || File.exist?(".git") then
148
+ scm = File.exist?(".svn") ? :svn : :git
149
+ yield scm
150
+ else
151
+ yield :none
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ def self.extract_latest_version url, matching=nil
159
+ file = URI.parse(url).read
160
+ versions = file.scan(/href="(ruby.*tar.gz)"/).flatten.reject { |s|
161
+ s =~ /preview|-rc\d/
162
+ }.sort_by { |s|
163
+ s.split(/\D+/).map { |i| i.to_i }
164
+ }.flatten
165
+ versions = versions.grep(/#{Regexp.escape(matching)}/) if matching
166
+ versions.last
167
+ end
168
+
169
+ def self.fetch_tar v
170
+ in_versions_dir do
171
+ warn " Determining latest version for #{v}"
172
+ ver = v[/\d+\.\d+/]
173
+ base = extract_latest_version("#{RUBY_URL}/#{ver}/", v)
174
+ abort "Could not determine release for #{v}" unless base
175
+ url = File.join RUBY_URL, ver, base
176
+ unless File.file? base then
177
+ warn " Fetching #{base} via HTTP... this might take a while."
178
+ open(url) do |f|
179
+ File.open base, 'w' do |out|
180
+ out.write f.read
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ def self.git_clone url, dir
188
+ Multiruby.in_versions_dir do
189
+ Multiruby.run "git clone #{url} #{dir}" unless File.directory? dir
190
+ FileUtils.ln_sf "../versions/#{dir}", "../build/#{dir}"
191
+ end
192
+ end
193
+
194
+ def self.gnu_utils_build inst_dir
195
+ run "autoconf" unless test ?f, "configure"
196
+ run "./configure --enable-shared --prefix #{inst_dir}", "log.configure" unless
197
+ test ?f, "Makefile"
198
+ run "(nice make -j4; nice make)", "log.build"
199
+ run "make install", "log.install"
200
+ end
201
+
202
+ def self.help
203
+ puts HELP.join
204
+ end
205
+
206
+ def self.in_build_dir
207
+ Dir.chdir File.join(self.root_dir, "build") do
208
+ yield
209
+ end
210
+ end
211
+
212
+ def self.in_install_dir
213
+ Dir.chdir File.join(self.root_dir, "install") do
214
+ yield
215
+ end
216
+ end
217
+
218
+ def self.in_root_dir
219
+ Dir.chdir self.root_dir do
220
+ yield
221
+ end
222
+ end
223
+
224
+ def self.in_tmp_dir
225
+ Dir.chdir File.join(self.root_dir, "tmp") do
226
+ yield
227
+ end
228
+ end
229
+
230
+ def self.in_versions_dir
231
+ Dir.chdir File.join(self.root_dir, "versions") do
232
+ yield
233
+ end
234
+ end
235
+
236
+ def self.list
237
+ puts "Known versions:"
238
+ in_install_dir do
239
+ Dir["*"].sort.each do |d|
240
+ puts " #{d}"
241
+ end
242
+ end
243
+ end
244
+
245
+ def self.merge_rubygems
246
+ in_install_dir do
247
+ gems = Dir["*/lib/ruby/gems"]
248
+
249
+ unless test ?d, "../gems" then
250
+ FileUtils.mv gems.first, ".."
251
+ end
252
+
253
+ gems.each do |d|
254
+ FileUtils.rm_rf d
255
+ FileUtils.ln_sf "../../../../gems", d
256
+ end
257
+ end
258
+ end
259
+
260
+ def self.mri_latest_tag v
261
+ Multiruby.tags.grep(/#{v}/).last
262
+ end
263
+
264
+ def self.rake_build inst_dir
265
+ run "rake", "log.build"
266
+ FileUtils.ln_sf "../build/#{File.basename Dir.pwd}", inst_dir
267
+ end
268
+
269
+ def self.rbx_ln dir
270
+ dir = File.expand_path dir
271
+ Multiruby.in_versions_dir do
272
+ FileUtils.ln_sf dir, "rubinius"
273
+ FileUtils.ln_sf "../versions/rubinius", "../install/rubinius"
274
+ end
275
+ end
276
+
277
+ def self.rm name
278
+ Multiruby.in_root_dir do
279
+ FileUtils.rm_rf Dir["*/#{name}"]
280
+ f = "versions/ruby-#{name}.tar.gz"
281
+ File.unlink f if test ?f, f
282
+ end
283
+ end
284
+
285
+ def self.root_dir
286
+ root_dir = File.expand_path(ENV['MULTIRUBY'] ||
287
+ File.join(ENV['HOME'], ".multiruby"))
288
+
289
+ unless test ?d, root_dir then
290
+ puts "creating #{root_dir}"
291
+ Dir.mkdir root_dir, 0700
292
+ end
293
+
294
+ root_dir
295
+ end
296
+
297
+ def self.run base_cmd, log = nil
298
+ cmd = base_cmd
299
+ cmd += " > #{log} 2>&1" if log
300
+ puts "Running command: #{cmd}"
301
+ raise "ERROR: Command failed with exit code #{$?}" unless system cmd
302
+ end
303
+
304
+ def self.setup_dirs download = true
305
+ %w(build install versions tmp).each do |dir|
306
+ unless test ?d, dir then
307
+ puts "creating #{dir}"
308
+ Dir.mkdir dir
309
+ if dir == "versions" && download then
310
+ warn " Downloading initial ruby tarballs to ~/.multiruby/versions:"
311
+ VERSIONS.each do |v|
312
+ self.fetch_tar v
313
+ end
314
+ warn " ...done"
315
+ warn " Put other ruby tarballs in ~/.multiruby/versions to use them."
316
+ end
317
+ end
318
+ end
319
+ end
320
+
321
+ def self.svn_co url, dir
322
+ Multiruby.in_versions_dir do
323
+ Multiruby.run "svn co #{url} #{dir}" unless File.directory? dir
324
+ FileUtils.ln_sf "../versions/#{dir}", "../build/#{dir}"
325
+ end
326
+ end
327
+
328
+ def self.tags
329
+ tags = nil
330
+ Multiruby.in_tmp_dir do
331
+ cache = "svn.tag.cache"
332
+ File.unlink cache if Time.now - File.mtime(cache) > 3600 rescue nil
333
+
334
+ File.open cache, "w" do |f|
335
+ f.write `svn ls #{MRI_SVN}/tags/`
336
+ end unless File.exist? cache
337
+
338
+ tags = File.read(cache).split(/\n/).grep(/^v/).reject {|s| s =~ /preview/}
339
+ end
340
+
341
+ tags = tags.sort_by { |s| s.scan(/\d+/).map { |s| s.to_i } }
342
+ end
343
+
344
+ def self.update
345
+ # TODO:
346
+ # update will look at the dir name and act accordingly rel_.* will
347
+ # figure out latest tag on that name and svn sw to it trunk and
348
+ # others will just svn update
349
+
350
+ clean = []
351
+
352
+ self.each_scm_build_dir do |style|
353
+ dir = File.basename(Dir.pwd)
354
+ warn dir
355
+
356
+ case style
357
+ when :svn then
358
+ case dir
359
+ when /mri_\d/ then
360
+ system "svn cleanup" # just in case
361
+ svn_up = `svn up`
362
+ in_build_dir do
363
+ if svn_up =~ /^[ADUCG] / then
364
+ clean << dir
365
+ else
366
+ warn " no update"
367
+ end
368
+ FileUtils.ln_sf "../build/#{dir}", "../versions/#{dir}"
369
+ end
370
+ when /mri_rel_(.+)/ then
371
+ ver = $1
372
+ url = `svn info`[/^URL: (.*)/, 1]
373
+ latest = self.mri_latest_tag(ver).chomp('/')
374
+ new_url = File.join(File.dirname(url), latest)
375
+ if new_url != url then
376
+ run "svn sw #{new_url}"
377
+ clean << dir
378
+ else
379
+ warn " no update"
380
+ end
381
+ else
382
+ warn " update in this svn dir not supported yet: #{dir}"
383
+ end
384
+ when :git then
385
+ case dir
386
+ when /rubinius/ then
387
+ run "rake git:update build" # minor cheat by building here
388
+ else
389
+ warn " update in this git dir not supported yet: #{dir}"
390
+ end
391
+ else
392
+ warn " update in non-svn dir not supported yet: #{dir}"
393
+ end
394
+ end
395
+
396
+ in_install_dir do
397
+ clean.each do |dir|
398
+ warn "removing install/#{dir}"
399
+ FileUtils.rm_rf dir
400
+ end
401
+ end
402
+ end
403
+
404
+ def self.update_rubygems
405
+ warn " Determining latest version for rubygems"
406
+ html = URI.parse(GEM_URL).read
407
+
408
+ versions = html.scan(/href="rubygems-update-(\d+(?:\.\d+)+).gem/i).flatten
409
+ latest = versions.sort_by { |s| s.scan(/\d+/).map { |s| s.to_i } }.last
410
+
411
+ Multiruby.in_versions_dir do
412
+ file = "rubygems-#{latest}.tgz"
413
+ unless File.file? file then
414
+ warn " Fetching rubygems-#{latest}.tgz via HTTP."
415
+ File.unlink(*Dir["rubygems*"])
416
+ File.open file, 'w' do |f|
417
+ f.write URI.parse(GEM_URL+"/"+file).read
418
+ end
419
+ end
420
+ end
421
+
422
+ Multiruby.in_install_dir do
423
+ FileUtils.rm_rf Dir["*"]
424
+ end
425
+ end
426
+ end
data/lib/zentest.rb ADDED
@@ -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 = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
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