1
# $Id: testmanager.rb,v 1.62 2003/12/16 16:36:35 sdalu Exp $
4
# CONTACT : zonecheck@nic.fr
5
# AUTHOR : Stephane D'Alu <sdalu@nic.fr>
7
# CREATED : 02/08/02 13:58:17
8
# REVISION : $Revision: 1.62 $
9
# DATE : $Date: 2003/12/16 16:36:35 $
11
# CONTRIBUTORS: (see also CREDITS file)
14
# LICENSE : GPL v2 (or MIT/X11-like after agreement)
15
# COPYRIGHT : AFNIC (c) 2003
17
# This file is part of ZoneCheck.
19
# ZoneCheck is free software; you can redistribute it and/or modify it
20
# under the terms of the GNU General Public License as published by
21
# the Free Software Foundation; either version 2 of the License, or
22
# (at your option) any later version.
24
# ZoneCheck is distributed in the hope that it will be useful, but
25
# WITHOUT ANY WARRANTY; without even the implied warranty of
26
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27
# General Public License for more details.
29
# You should have received a copy of the GNU General Public License
30
# along with ZoneCheck; if not, write to the Free Software Foundation,
31
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
43
## TODO: decide how to replace the Errno::EADDRNOTAVAIL which is not
44
## available on windows
45
## TODO: improved detection of dependencies issues
47
## attributs: param, classes, cm, config, tests
49
TestSuperclass = Test # Superclass
50
TestPrefix = 'tst_' # Prefix for test methods
51
CheckPrefix = 'chk_' # Prefix for check methods
54
## Exception: error in the test definition
56
class DefinitionError < StandardError
61
# List of loaded test files
62
# (avoid loading the same file twice)
67
# Load ruby files implementing tests
68
# WARN: we are required to untaint for loading
69
# WARN: file are only loaded once to avoid redefinition of constants
71
# To minimize risk of choosing a random directory, only files
72
# that have the ruby extension (.rb) and the 'ZCTEST 1.0'
73
# magic header are loaded.
75
def self.load(*filenames)
77
filenames.each { |filename|
78
# Recursively load file in the directory
79
if File.directory?(filename)
80
$dbg.msg(DBG::LOADING) { "test directory: #{filename}" }
81
Dir::open(filename) { |dir|
83
testfile = "#{filename}/#{entry}".untaint
84
count += self.load(testfile) if File.file?(testfile)
89
elsif File.file?(filename)
90
# Only load file if it meet some criteria (see above)
91
if ((filename =~ /\.rb$/) &&
93
File.open(filename) { |io|
94
io.gets =~ /^\#\s*ZCTEST\s+1\.0:?\W/ }
95
rescue # XXX: Careful with rescue all
99
# Really load the file if it wasn't already done
100
if ! @@test_files.has_key?(filename)
101
$dbg.msg(DBG::LOADING) { "test file: #{filename}" }
102
::Kernel.load filename
103
@@test_files[filename] = true
106
$dbg.msg(DBG::LOADING) {
107
"test file: #{filename} (already loaded)" }
113
# Return the number of loaded file
119
# Initialize a new object.
122
@tests = {} # Hash of test method name (tst_*)
123
@checks = {} # Hash of check method name (chk_*)
124
@classes = [] # List of classes used by the methods above
131
# Add all the available classes that containts test/check methods
134
# Add the test classes (they should have Test as superclass)
135
[ CheckGeneric, CheckNameServer,
136
CheckNetworkAddress, CheckExtra].each { |mod|
137
mod.constants.each { |t|
138
testclass = eval "#{mod}::#{t}"
139
if testclass.superclass == TestSuperclass
140
$dbg.msg(DBG::TESTS) { "adding class: #{testclass}" }
143
$dbg.msg(DBG::TESTS) { "skipping class: #{testclass}" }
151
# Register all the tests/checks that are provided by the class 'klass'.
154
# Sanity check (all test class should derive from Test)
155
if ! (klass.superclass == TestSuperclass)
157
$mc.get('xcp_testmanager_badclass') % [ klass, TestSuperclass ]
160
# Inspect instance methods for finding methods (ie: chk_*, tst_*)
161
klass.public_instance_methods(true).each { |method|
163
# methods that represent a test
164
when /^#{TestPrefix}(.*)/
166
if has_test?(testname)
167
l10n_tag = $mc.get('xcp_testmanager_test_exists')
168
raise DefinitionError,
169
l10n_tag % [ testname, klass, @tests[testname] ]
171
@tests[testname] = klass
173
# methods that represent a check
174
when /^#{CheckPrefix}(.*)/
176
if has_check?(checkname)
177
l10n_tag = $mc.get('xcp_testmanager_check_exists')
178
raise DefinitionError,
179
l10n_tag % [ checkname, klass, @tests[checkname] ]
181
@checks[checkname] = klass
185
# Add it to the list of classes
186
# The class will be unique in the list otherwise the checking
187
# above will fail with method defined twice.
193
# Check if 'test' has already been registered.
195
def has_test?(testname)
196
@tests.has_key?(testname)
201
# Check if 'check' has already been registered.
203
def has_check?(checkname)
204
@checks.has_key?(checkname)
210
def wanted_check?(checkname, category)
211
return true unless @param.test.categories
213
@param.test.categories.each { |rule|
214
if (rule[0] == ?! || rule[0] == ?-)
215
negation, name = true, rule[1..-1]
216
elsif (rule[0] == ?+)
217
negation, name = false, rule[1..-1]
219
negation, name = false, rule
222
return !negation if name.empty?
224
if ((name == category) ||
225
!(category =~ /^#{Regexp.escape(name)}:/).nil?)
233
# Return check family (ie: generic, nameserver, address, extra)
235
def family(checkname)
236
klass = @checks[checkname]
237
klass.name =~ /^([^:]+)/
243
# Return list of available checks
251
# Use the configuration object ('config') to instanciate each
252
# class (but only once) that will be used to perform the tests.
254
def init(config, cm, param, do_preeval=true)
257
@publisher = @param.publisher.engine
260
@do_preeval = do_preeval
265
CheckExtra.family => proc { |bl| bl.call },
266
CheckGeneric.family => proc { |bl| bl.call },
267
CheckNameServer.family => proc { |bl|
268
@param.domain.ns.each { |ns_name, | bl.call(ns_name) } },
269
CheckNetworkAddress.family => proc { |bl|
270
@param.domain.ns.each { |ns_name, ns_addr_list|
271
@param.network.address_wanted?(ns_addr_list).each { |addr|
272
bl.call(ns_name, addr) } } }
275
# Create new instance of the class
276
@classes.each { |klass|
277
@objects[klass] = klass.method('new').call(@param.network, @config,
284
# Perform unitary check
286
def check1(checkname, severity, ns=nil, ip=nil)
287
# Build argument list
289
args << ns if !ns.nil?
290
args << ip if !ip.nil?
293
$dbg.msg(DBG::TESTS) {
294
where = args.empty? ? "generic" : args.join('/')
295
"checking: #{checkname} [#{where}]" }
298
@param.info.testcount += 1
300
# Retrieve the method representing the check
301
klass = @checks[checkname]
302
object = @objects[klass]
303
method = object.method(CheckPrefix + checkname)
305
# Retrieve information relative to the test output
306
sev_report = case severity
307
when Config::Fatal then @param.report.fatal
308
when Config::Warning then @param.report.warning
309
when Config::Info then @param.report.info
312
# Publish information about the test being executed
313
@publisher.progress.process(checkname, ns, ip)
316
desc = Test::Result::Desc::new
317
result_class = Test::Error
319
starttime = Time::now
322
data = method.call(*args)
324
exectime = Time::now - starttime
326
desc.details = data if data
327
result_class = case data
328
when NilClass, FalseClass, Hash then Test::Failed
331
rescue NResolv::DNS::ReplyError => e
332
info = "(#{e.resource.rdesc}: #{e.name})"
334
when NResolv::DNS::RCode::SERVFAIL
335
$mc.get('nresolv:rcode:servfail')
336
when NResolv::DNS::RCode::REFUSED
337
$mc.get('nresolv:rcode:refused')
338
when NResolv::DNS::RCode::NXDOMAIN
339
$mc.get('nresolv:rcode:nxdomain')
340
when NResolv::DNS::RCode::NOTIMP
341
$mc.get('nresolv:rcode:notimp')
344
desc.error = "#{name} #{info}"
345
# rescue Errno::EADDRNOTAVAIL
346
# desc.err = "Network transport unavailable try option -4 or -6"
347
rescue NResolv::TimeoutError => e
348
desc.error = "DNS Timeout"
349
rescue Timeout::Error => e
350
desc.error = "Timeout"
351
rescue NResolv::NResolvError => e
352
desc.error = "Resolver error (#{e})"
353
rescue ZCMail::ZCMailError => e
354
desc.error = "Mail error (#{e})"
355
rescue Exception => e
356
# XXX: this is a hack
357
unless @param.rflag.stop_on_fatal
358
desc.error = 'Dependency issue? (allwarning/dontstop flag?)'
360
desc.error = e.message
362
raise if $dbg.enabled?(DBG::DONT_RESCUE)
364
$dbg.msg(DBG::TESTS) {
365
resstr = result_class.to_s.gsub(/^.*::/, '')
366
where = args.empty? ? 'generic' : args.join('/')
367
timestr = "%.2f" % exectime
368
"result: #{resstr} for #{checkname} [#{where}] (in #{timestr} sec)"
374
result = result_class::new(checkname, desc, ns, ip)
376
rescue Report::FatalError
377
raise if @param.rflag.stop_on_fatal
383
# Perform unitary test
385
def test1(testname, report=true, ns=nil, ip=nil)
386
$dbg.msg(DBG::TESTS) { "test: #{testname}" }
387
@cache.use(:test, [ testname, ns, ip ]) {
388
# Retrieve the method representing the test
389
klass = @tests[testname]
390
object = @objects[klass]
391
method = object.method(TestPrefix + testname)
395
args << ns unless ns.nil?
396
args << ip unless ip.nil?
399
rescue NResolv::NResolvError => e
400
return e unless report
401
desc = Test::Result::Desc::new(false)
402
desc.error = "Resolver error (#{e})"
403
@param.report.fatal << Test::Error::new(testname, desc, ns, ip)
409
# Perform all the tests as asked in the configuration file and
410
# according to the program parameters
415
domainname_s = @param.domain.name.to_s
416
starttime = Time::now
419
@param.info.nscount = @param.domain.ns.size
421
# Do a pre-evaluation of the code
423
# Sanity check for debugging
424
if $dbg.enabled?(DBG::NOCACHE)
425
raise 'Debugging with preeval and NOCACHE is not adviced'
428
# Do the pre-evaluation
429
# => compute the number of checking to perform
431
Config::TestSeqOrder.each { |family|
432
next unless rules = @config.rules[family]
434
@iterer[family].call(proc { |*args|
435
testcount += rules.preeval(self, args)
438
rescue Instruction::InstructionError => e
439
$dbg.msg(DBG::TESTS) { "disabling preeval: #{e}" }
448
@publisher.progress.start(testcount)
450
# Perform the checking
451
Config::TestSeqOrder.each { |family|
452
next unless rules = @config.rules[family]
455
@iterer[family].call(proc { |*args|
456
threadlist << Thread::new {
458
rules.eval(self, args)
459
rescue Report::FatalError
461
rescue Exception => e
463
puts "Exception #{e.message}"
470
threadlist.each { |thr| thr.join }
473
# Counter final status
474
if @param.report.fatal.empty?
475
then @publisher.progress.done(domainname_s)
476
else @publisher.progress.failed(domainname_s)
479
rescue Report::FatalError
480
if @param.report.fatal.empty?
481
raise "BUG: FatalError with no fatal error stored in report"
483
@publisher.progress.failed(domainname_s)
487
@publisher.progress.finish
489
@param.info.testingtime = Time::now - starttime
493
@param.report.fatal.empty?