~ubuntu-branches/ubuntu/trusty/zonecheck/trusty-proposed

« back to all changes in this revision

Viewing changes to zc/testmanager.rb

  • Committer: Bazaar Package Importer
  • Author(s): Stephane Bortzmeyer
  • Date: 2004-03-10 14:08:05 UTC
  • Revision ID: james.westby@ubuntu.com-20040310140805-ij55fso1e23bk8ye
Tags: upstream-2.0.3
ImportĀ upstreamĀ versionĀ 2.0.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# $Id: testmanager.rb,v 1.62 2003/12/16 16:36:35 sdalu Exp $
 
2
 
 
3
 
4
# CONTACT     : zonecheck@nic.fr
 
5
# AUTHOR      : Stephane D'Alu <sdalu@nic.fr>
 
6
#
 
7
# CREATED     : 02/08/02 13:58:17
 
8
# REVISION    : $Revision: 1.62 $ 
 
9
# DATE        : $Date: 2003/12/16 16:36:35 $
 
10
#
 
11
# CONTRIBUTORS: (see also CREDITS file)
 
12
#
 
13
#
 
14
# LICENSE     : GPL v2 (or MIT/X11-like after agreement)
 
15
# COPYRIGHT   : AFNIC (c) 2003
 
16
#
 
17
# This file is part of ZoneCheck.
 
18
#
 
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.
 
23
 
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.
 
28
#
 
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
 
32
#
 
33
 
 
34
require 'thread'
 
35
require 'timeout'
 
36
require 'time'
 
37
require 'framework'
 
38
require 'report'
 
39
require 'cache'
 
40
 
 
41
 
 
42
##
 
43
## TODO: decide how to replace the Errno::EADDRNOTAVAIL which is not
 
44
##       available on windows
 
45
## TODO: improved detection of dependencies issues
 
46
##
 
47
## attributs: param, classes, cm, config, tests
 
48
class TestManager
 
49
    TestSuperclass = Test       # Superclass
 
50
    TestPrefix     = 'tst_'     # Prefix for test methods
 
51
    CheckPrefix    = 'chk_'     # Prefix for check methods
 
52
 
 
53
    ##
 
54
    ## Exception: error in the test definition
 
55
    ##
 
56
    class DefinitionError < StandardError
 
57
    end
 
58
 
 
59
 
 
60
    #
 
61
    # List of loaded test files
 
62
    #  (avoid loading the same file twice)
 
63
    #
 
64
    @@test_files = {}
 
65
 
 
66
    #
 
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
 
70
    #
 
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.
 
74
    #
 
75
    def self.load(*filenames)
 
76
        count = 0
 
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|
 
82
                    dir.each { |entry|
 
83
                        testfile = "#{filename}/#{entry}".untaint
 
84
                        count += self.load(testfile) if File.file?(testfile)
 
85
                    }
 
86
                }
 
87
                
 
88
            # Load test file
 
89
            elsif File.file?(filename)
 
90
                # Only load file if it meet some criteria (see above)
 
91
                if ((filename =~ /\.rb$/) &&
 
92
                    begin
 
93
                        File.open(filename) { |io|
 
94
                            io.gets =~ /^\#\s*ZCTEST\s+1\.0:?\W/ }
 
95
                    rescue # XXX: Careful with rescue all
 
96
                        false
 
97
                    end)
 
98
 
 
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
 
104
                        count += 1
 
105
                    else
 
106
                        $dbg.msg(DBG::LOADING) {
 
107
                            "test file: #{filename} (already loaded)" }
 
108
                    end
 
109
                end
 
110
            end
 
111
        }
 
112
 
 
113
        # Return the number of loaded file
 
114
        return count
 
115
    end
 
116
 
 
117
 
 
118
    #
 
119
    # Initialize a new object.
 
120
    #
 
121
    def initialize
 
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
 
125
        @cache = Cache::new
 
126
        @cache.create(:test)
 
127
   end
 
128
 
 
129
 
 
130
    #
 
131
    # Add all the available classes that containts test/check methods
 
132
    #
 
133
    def add_allclasses
 
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}"   }
 
141
                    self << testclass
 
142
                else
 
143
                    $dbg.msg(DBG::TESTS) { "skipping class: #{testclass}" }
 
144
                end
 
145
            }
 
146
        }
 
147
    end
 
148
 
 
149
 
 
150
    #
 
151
    # Register all the tests/checks that are provided by the class 'klass'.
 
152
    #
 
153
    def <<(klass)
 
154
        # Sanity check (all test class should derive from Test)
 
155
        if ! (klass.superclass == TestSuperclass)
 
156
            raise ArgumentError, 
 
157
                $mc.get('xcp_testmanager_badclass') % [ klass, TestSuperclass ]
 
158
        end
 
159
        
 
160
        # Inspect instance methods for finding methods (ie: chk_*, tst_*)
 
161
        klass.public_instance_methods(true).each { |method|         
 
162
            case method
 
163
            # methods that represent a test
 
164
            when /^#{TestPrefix}(.*)/
 
165
                testname = $1
 
166
                if has_test?(testname)
 
167
                    l10n_tag = $mc.get('xcp_testmanager_test_exists')
 
168
                    raise DefinitionError, 
 
169
                        l10n_tag % [ testname, klass, @tests[testname] ]
 
170
                end
 
171
                @tests[testname] = klass
 
172
 
 
173
            # methods that represent a check
 
174
            when /^#{CheckPrefix}(.*)/
 
175
                checkname = $1
 
176
                if has_check?(checkname)
 
177
                    l10n_tag = $mc.get('xcp_testmanager_check_exists')
 
178
                    raise DefinitionError, 
 
179
                        l10n_tag % [ checkname, klass, @tests[checkname] ]
 
180
                end
 
181
                @checks[checkname] = klass
 
182
            end
 
183
        }
 
184
 
 
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.
 
188
        @classes << klass
 
189
    end
 
190
 
 
191
 
 
192
    #
 
193
    # Check if 'test' has already been registered.
 
194
    #
 
195
    def has_test?(testname)
 
196
        @tests.has_key?(testname)
 
197
    end
 
198
 
 
199
 
 
200
    #
 
201
    # Check if 'check' has already been registered.
 
202
    #
 
203
    def has_check?(checkname)
 
204
        @checks.has_key?(checkname)
 
205
    end
 
206
    
 
207
    #
 
208
    #
 
209
    #
 
210
    def wanted_check?(checkname, category)
 
211
        return true unless @param.test.categories
 
212
 
 
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]
 
218
            else
 
219
                negation, name = false, rule
 
220
            end
 
221
 
 
222
            return !negation if name.empty?
 
223
 
 
224
            if ((name == category) || 
 
225
                !(category =~ /^#{Regexp.escape(name)}:/).nil?)
 
226
                return !negation
 
227
            end
 
228
        }
 
229
        return false
 
230
    end
 
231
    
 
232
    #
 
233
    # Return check family (ie: generic, nameserver, address, extra)
 
234
    #
 
235
    def family(checkname) 
 
236
        klass = @checks[checkname]
 
237
        klass.name =~ /^([^:]+)/
 
238
        eval("#{$1}.family")
 
239
    end
 
240
 
 
241
 
 
242
    #
 
243
    # Return list of available checks
 
244
    #
 
245
    def list
 
246
        @checks.keys
 
247
    end
 
248
 
 
249
 
 
250
    #
 
251
    # Use the configuration object ('config') to instanciate each
 
252
    # class (but only once) that will be used to perform the tests.
 
253
    #
 
254
    def init(config, cm, param, do_preeval=true)
 
255
        @config         = config
 
256
        @param          = param
 
257
        @publisher      = @param.publisher.engine
 
258
        @objects        = {}
 
259
        @cm             = cm
 
260
        @do_preeval     = do_preeval
 
261
 
 
262
        @cache.clear(:test)
 
263
 
 
264
        @iterer = { 
 
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) } } }
 
273
        }
 
274
 
 
275
        # Create new instance of the class
 
276
        @classes.each { |klass|
 
277
            @objects[klass] = klass.method('new').call(@param.network, @config,
 
278
                                                       @cm, @param.domain)
 
279
        }
 
280
    end
 
281
 
 
282
 
 
283
    #
 
284
    # Perform unitary check
 
285
    #
 
286
    def check1(checkname, severity, ns=nil, ip=nil) 
 
287
        # Build argument list
 
288
        args = []
 
289
        args << ns if !ns.nil?
 
290
        args << ip if !ip.nil?
 
291
 
 
292
        # Debugging
 
293
        $dbg.msg(DBG::TESTS) {
 
294
            where  = args.empty? ? "generic" : args.join('/')
 
295
            "checking: #{checkname} [#{where}]" }
 
296
 
 
297
        # Stat
 
298
        @param.info.testcount += 1
 
299
 
 
300
        # Retrieve the method representing the check
 
301
        klass   = @checks[checkname]
 
302
        object  = @objects[klass]
 
303
        method  = object.method(CheckPrefix + checkname)
 
304
        
 
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
 
310
                     end
 
311
 
 
312
        # Publish information about the test being executed
 
313
        @publisher.progress.process(checkname, ns, ip)
 
314
 
 
315
        # Perform the test
 
316
        desc         = Test::Result::Desc::new
 
317
        result_class = Test::Error
 
318
        begin
 
319
            starttime    = Time::now
 
320
            exectime     = nil
 
321
            begin
 
322
                data     = method.call(*args)
 
323
            ensure
 
324
                exectime = Time::now - starttime
 
325
            end
 
326
            desc.details = data if data
 
327
            result_class = case data 
 
328
                           when NilClass, FalseClass, Hash then Test::Failed
 
329
                           else Test::Succeed
 
330
                           end
 
331
        rescue NResolv::DNS::ReplyError => e
 
332
            info = "(#{e.resource.rdesc}: #{e.name})"
 
333
            name = case e.code
 
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')
 
342
                   else e.code.to_s
 
343
                   end
 
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?)'
 
359
            else
 
360
                desc.error = e.message
 
361
            end
 
362
            raise if $dbg.enabled?(DBG::DONT_RESCUE)
 
363
        ensure
 
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)"
 
369
            }
 
370
        end
 
371
 
 
372
        # Build result
 
373
        begin
 
374
            result = result_class::new(checkname, desc, ns, ip)
 
375
            sev_report << result
 
376
        rescue Report::FatalError
 
377
            raise if @param.rflag.stop_on_fatal
 
378
        end
 
379
    end
 
380
 
 
381
 
 
382
    #
 
383
    # Perform unitary test
 
384
    #
 
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)
 
392
            
 
393
            # Call the method
 
394
            args = []
 
395
            args << ns unless ns.nil?
 
396
            args << ip unless ip.nil?
 
397
            begin
 
398
                method.call(*args)
 
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)
 
404
            end
 
405
        }
 
406
    end
 
407
 
 
408
    #
 
409
    # Perform all the tests as asked in the configuration file and
 
410
    # according to the program parameters
 
411
    #
 
412
    def check
 
413
        threadlist      = []
 
414
        testcount       = 0
 
415
        domainname_s    = @param.domain.name.to_s
 
416
        starttime       = Time::now
 
417
 
 
418
        # Stats
 
419
        @param.info.nscount = @param.domain.ns.size
 
420
 
 
421
        # Do a pre-evaluation of the code
 
422
        if @do_preeval
 
423
            # Sanity check for debugging
 
424
            if $dbg.enabled?(DBG::NOCACHE)
 
425
                raise 'Debugging with preeval and NOCACHE is not adviced'
 
426
            end
 
427
 
 
428
            # Do the pre-evaluation
 
429
            #  => compute the number of checking to perform
 
430
            begin
 
431
                Config::TestSeqOrder.each { |family|
 
432
                    next unless rules = @config.rules[family]
 
433
                    
 
434
                    @iterer[family].call(proc { |*args|
 
435
                                testcount += rules.preeval(self, args)
 
436
                           })
 
437
                }
 
438
            rescue Instruction::InstructionError => e
 
439
                $dbg.msg(DBG::TESTS) { "disabling preeval: #{e}" }
 
440
                @do_preeval = false
 
441
                testcount   = 0
 
442
            end
 
443
        end
 
444
 
 
445
        # Perform the tests
 
446
        begin
 
447
            # Counter start
 
448
            @publisher.progress.start(testcount)
 
449
 
 
450
            # Perform the checking
 
451
            Config::TestSeqOrder.each { |family|
 
452
                next unless rules = @config.rules[family]
 
453
 
 
454
                threadlist      = []
 
455
                @iterer[family].call(proc { |*args|
 
456
                        threadlist << Thread::new {
 
457
                            begin
 
458
                                rules.eval(self, args)
 
459
                            rescue Report::FatalError
 
460
                                raise
 
461
                            rescue Exception => e
 
462
                                # XXX: debuging
 
463
                                puts "Exception #{e.message}"
 
464
                                puts e.backtrace
 
465
                                raise
 
466
                            end
 
467
                        }
 
468
                    })
 
469
 
 
470
                threadlist.each { |thr| thr.join }
 
471
            }
 
472
 
 
473
            # Counter final status
 
474
            if @param.report.fatal.empty?
 
475
            then @publisher.progress.done(domainname_s)
 
476
            else @publisher.progress.failed(domainname_s)
 
477
            end
 
478
 
 
479
        rescue Report::FatalError
 
480
            if @param.report.fatal.empty?
 
481
                raise "BUG: FatalError with no fatal error stored in report"
 
482
            end
 
483
            @publisher.progress.failed(domainname_s)
 
484
 
 
485
        ensure
 
486
            # Counter cleanup
 
487
            @publisher.progress.finish
 
488
            # Total testing time
 
489
            @param.info.testingtime = Time::now - starttime
 
490
        end
 
491
 
 
492
        # Status
 
493
        @param.report.fatal.empty?
 
494
    end
 
495
end