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/.autotest +21 -0
- data/.gitignore +1 -0
- data/History.txt +631 -0
- data/README.markdown +96 -0
- data/Rakefile +70 -0
- data/VERSION +1 -0
- data/bin/multigem +4 -0
- data/bin/multiruby +50 -0
- data/bin/multiruby_setup +73 -0
- data/bin/zentest +28 -0
- data/example_dot_autotest.rb +12 -0
- data/lib/focus.rb +21 -0
- data/lib/functional_test_matrix.rb +92 -0
- data/lib/multiruby.rb +426 -0
- data/lib/zentest.rb +576 -0
- data/lib/zentest_mapping.rb +117 -0
- data/test/helper.rb +6 -0
- data/test/test_focus.rb +34 -0
- data/test/test_zentest.rb +560 -0
- data/test/test_zentest_mapping.rb +238 -0
- data/zentest-without-autotest.gemspec +62 -0
- metadata +81 -0
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
|