zentest-without-autotest 4.1.4

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.
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