File: testmanager.rb

package info (click to toggle)
zonecheck 3.0.2-1
  • links: PTS
  • area: main
  • in suites: squeeze
  • size: 1,312 kB
  • ctags: 836
  • sloc: ruby: 6,664; xml: 693; sh: 518; python: 301; makefile: 75
file content (521 lines) | stat: -rw-r--r-- 14,918 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
# $Id: testmanager.rb,v 1.68 2010/06/14 11:47:54 chabannf Exp $

# 
# CONTACT     : zonecheck@nic.fr
# AUTHOR      : Stephane D'Alu <sdalu@nic.fr>
#
# CREATED     : 02/08/02 13:58:17
# REVISION    : $Revision: 1.68 $ 
# DATE        : $Date: 2010/06/14 11:47:54 $
#
# CONTRIBUTORS: (see also CREDITS file)
#
#
# LICENSE     : GPL v3
# COPYRIGHT   : AFNIC (c) 2003
#
# This file is part of ZoneCheck.
#
# ZoneCheck is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# 
# ZoneCheck is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ZoneCheck; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
module ZoneCheck
require 'thread'
require 'timeout'
require 'time'
require 'framework'
require 'report'
require 'cache'


##
## TODO: decide how to replace the Errno::EADDRNOTAVAIL which is not
##       available on windows
## TODO: improved detection of dependencies issues
##
## attributs: param, classes, cm, config, tests
class TestManager
  TestSuperclass = Test	# Superclass
  TestPrefix     = 'tst_'	# Prefix for test methods
  CheckPrefix    = 'chk_'	# Prefix for check methods
  
  ##
  ## Exception: error in the test definition
  ##
  class DefinitionError < StandardError
  end
  
  
  #
  # List of loaded test files
  #  (avoid loading the same file twice)
  #
  @@test_files = {}
  
  #
  # Load ruby files implementing tests
  #  WARN: we are required to untaint for loading
  #  WARN: file are only loaded once to avoid redefinition of constants
  #
  # To minimize risk of choosing a random directory, only files
  #  that have the ruby extension (.rb) and the 'ZCTEST 1.0'
  #  magic header are loaded.
  #
  def self.load(*filenames)
    count = 0
    filenames.each { |filename|
      # Recursively load file in the directory
      if File.directory?(filename)
        $dbg.msg(DBG::LOADING) { "test directory: #{filename}" }
        Dir::open(filename) { |dir|
          dir.each { |entry|
            testfile = "#{filename}/#{entry}".untaint
            count += self.load(testfile) if File.file?(testfile)
          }
        }
        
        # Load test file
      elsif File.file?(filename)
        # Only load file if it meet some criteria (see above)
        if ((filename =~ /\.rb$/) &&
        begin
          File.open(filename) { |io|
          io.gets =~ /^\#\s*ZCTEST\s+1\.0:?\W/ }
        rescue # XXX: Careful with rescue all
          false
        end)
        
          # Really load the file if it wasn't already done
          if  ! @@test_files.has_key?(filename)
          $dbg.msg(DBG::LOADING) { "test file: #{filename}" }
          ::Kernel.load filename
          @@test_files[filename] = true
          count += 1
          else
          $dbg.msg(DBG::LOADING) {
          "test file: #{filename} (already loaded)" }
          end
        end
      end
    }
    
    # Return the number of loaded file
    return count
  end
  
  
  #
  # Initialize a new object.
  #
  def initialize
    @tests		= {}	# Hash of test  method name (tst_*)
    @checks		= {}	# Hash of check method name (chk_*)
    @classes	= []	# List of classes used by the methods above
    @cache = Cache::new
    @cache.create(:test)
  end
  
  
  #
  # Add all the available classes that containts test/check methods
  #
  def add_allclasses
    # Add the test classes (they should have Test as superclass)
    [ CheckGeneric, CheckNameServer, 
      CheckNetworkAddress, CheckExtra, CheckDNSSEC].each { |mod|
        mod.constants.each { |t|
          testclass = eval "#{mod}::#{t}"
          if testclass.superclass == TestSuperclass
            $dbg.msg(DBG::TESTS) { "adding class: #{testclass}"   }
            self << testclass
          else
            $dbg.msg(DBG::TESTS) { "skipping class: #{testclass}" }
          end
        }
    }
  end
  
  
  #
  # Register all the tests/checks that are provided by the class 'klass'.
  #
  def <<(klass)
    # Sanity check (all test class should derive from Test)
    if ! (klass.superclass == TestSuperclass)
      raise ArgumentError, 
      $mc.get('xcp_testmanager_badclass') % [ klass, TestSuperclass ]
    end
    
    # Inspect instance methods for finding methods (ie: chk_*, tst_*)
    klass.public_instance_methods(true).each { |method| 	    
      case method
      # methods that represent a test
        when /^#{TestPrefix}(.*)/
          testname = $1
          if has_test?(testname)
            l10n_tag = $mc.get('xcp_testmanager_test_exists')
            raise DefinitionError, 
            l10n_tag % [ testname, klass, @tests[testname] ]
          end
          @tests[testname] = klass
        
        # methods that represent a check
        when /^#{CheckPrefix}(.*)/
          checkname = $1
          if has_check?(checkname)
            l10n_tag = $mc.get('xcp_testmanager_check_exists')
            raise DefinitionError, 
            l10n_tag % [ checkname, klass, @tests[checkname] ]
          end
          @checks[checkname] = klass
      end
    }
    
    # Add it to the list of classes
    #  The class will be unique in the list otherwise the checking
    #  above will fail with method defined twice.
    @classes << klass
  end
  
  
  #
  # Check if 'test' has already been registered.
  #
  def has_test?(testname)
    @tests.has_key?(testname)
  end
  
  
  #
  # Check if 'check' has already been registered.
  #
  def has_check?(checkname)
    @checks.has_key?(checkname)
  end
    
  #
  #
  #
  def wanted_check?(checkname, category)
    return true unless @param.test.categories
    
    @param.test.categories.each { |rule|
      if    (rule[0] == ?! || rule[0] == ?-)
        negation, name = true,  rule[1..-1]
      elsif (rule[0] == ?+)
        negation, name = false, rule[1..-1]
      else
        negation, name = false, rule
      end
      
      return !negation if name.empty?
      
      if ((name == category) || 
        !(category =~ /^#{Regexp.escape(name)}:/).nil?)
          return !negation
      end
    }
    return false
  end
    
  #
  # Return check family (ie: generic, nameserver, address, extra, dnssec)
  #
  def family(checkname) 
    klass = @checks[checkname]
    klass.name =~ /^((ZoneCheck::[^:]+)|([^:]+))/
    eval("#{$1}.family")
  end
  
  
  #
  # Return list of available checks
  #
  def list
    @checks.keys
  end
  
  
  #
  # Use the configuration object ('config') to instanciate each
  # class (but only once) that will be used to perform the tests.
  #
  def init(config, cm, param, do_preeval=true)
    @config		= config
    @param		= param
    @publisher	= @param.publisher.engine
    @objects	= {}
    @cm		= cm
    @do_preeval	= do_preeval
    
    @cache.clear(:test)
    
    @iterer = { 
      CheckExtra.family          => proc { |bl| bl.call },
      CheckGeneric.family        => proc { |bl| bl.call },
      CheckNameServer.family     => proc { |bl| 
        @param.domain.ns.each { |ns_name, | bl.call(ns_name) } 
      },
      CheckNetworkAddress.family => proc { |bl| 
        @param.domain.ns.each { |ns_name, ns_addr_list|
          @param.network.address_wanted?(ns_addr_list).each { |addr|
              bl.call(ns_name, addr) }
        }
      },
      CheckDNSSEC.family     => proc { |bl| 
        @param.domain.ns.each { |ns_name, ns_addr_list|
          @param.network.address_wanted?(ns_addr_list).each { |addr|
              bl.call(ns_name, addr) }
        }
      }
    }
    
    # Create new instance of the class
    @classes.each { |klass|
      @objects[klass] = klass.method('new').call(@param.network, @config,
                                                 @cm, @param.domain)
    }
  end
  
  
  #
  # Perform unitary check
  #
  def check1(checkname, severity, ns=nil, ip=nil) 
    # Build argument list
    #      puts "checkname: #{checkname}"
    args = []
    args << ns if !ns.nil?
    args << ip if !ip.nil?
    
    # Debugging
    $dbg.msg(DBG::TESTS) {
      where  = args.empty? ? "generic" : args.collect{|e| e.to_s}.join('/')
      "checking: #{checkname} [#{where}]" 
    }
    
    # Stat
    @param.info.testcount += 1
    
    # Retrieve the method representing the check
    klass   = @checks[checkname]
    object  = @objects[klass]
    method  = object.method(CheckPrefix + checkname)
    
    # Retrieve information relative to the test output
    sev_report = case severity
                 when  Config::Fatal   then @param.report.fatal
                 when  Config::Warning then @param.report.warning
                 when  Config::Info    then @param.report.info
                 end
    
    # Publish information about the test being executed
    @publisher.progress.process(checkname, ns, ip)
    
    # Perform the test
    desc         = Test::Result::Desc::new
    result_class = Test::Error
    begin
      starttime    = Time::now
      exectime     = nil
      begin
        data     = method.call(*args)
      ensure
        exectime = Time::now - starttime
      end
      desc.details = data if data
      result_class = case data 
                     when NilClass, FalseClass, Hash then Test::Failed
                     else Test::Succeed
                     end
    rescue Dnsruby::NXDomain => e
      # TODO
      #	    info = "(#{e.resource.rdesc}: #{e.name})"
      name = $mc.get('resolv:rcode:nxdomain')
      desc.error = "#{name}: #{e.message}"# #{info}"
    rescue Dnsruby::ServFail => e
      # TODO
      #      info = "(#{e.resource.rdesc}: #{e.name})"
      name = $mc.get('resolv:rcode:servfail')
      desc.error = "#{name}: #{e.message}" # #{info}" 
    rescue Dnsruby::Refused => e
      # TODO
      #      info = "(#{e.resource.rdesc}: #{e.name})"
      name = $mc.get('resolv:rcode:refused')
      desc.error = "#{name}: #{e.message}" # #{info}"  
    rescue Dnsruby::NotImp => e
      # TODO
      #      info = "(#{e.resource.rdesc}: #{e.name})"
      name = $mc.get('resolv:rcode:notimp') 
      desc.error = "#{name}: #{e.message}" # #{info}"
    rescue Errno::EADDRNOTAVAIL
      desc.error = "Network transport unavailable try option -4 or -6"
    rescue Dnsruby::ResolvTimeout => e
      desc.error = "DNS Timeout"
    rescue Timeout::Error => e
      desc.error = "Timeout"
    rescue Dnsruby::ResolvError => e
      desc.error = "Resolver error (#{e.message})"
    rescue  ZoneCheck::Mail::MailError => e
      desc.error = "Mail error (#{e})"
    rescue Exception => e
      # XXX: this is a hack
#      unless @param.rflag.stop_on_fatal
#      desc.error = 'Dependency issue? (allwarning/dontstop flag?)'
#      else
      desc.error = e.message
#      end
      raise if $dbg.enabled?(DBG::DONT_RESCUE)
    ensure
      $dbg.msg(DBG::TESTS) { 
      resstr  = result_class.to_s.gsub(/^.*::/, '')
      where   = args.empty? ? 'generic' : args.collect{|e| e.to_s}.join('/')
      timestr = "%.2f" % exectime
      "result: #{resstr} for #{checkname} [#{where}] (in #{timestr} sec)"
      }
    end
    
    # Build result
    begin
      result = result_class::new(checkname, desc, ns, ip)
      sev_report << result
    rescue Report::FatalError
      raise if @param.rflag.stop_on_fatal
    end
    
    return result_class
  end
  
  
  #
  # Perform unitary test
  #
  def test1(testname, report=true, ns=nil, ip=nil)
    $dbg.msg(DBG::TESTS) { "test: #{testname}" }
    @cache.use(:test, [ testname, ns, ip ]) {
      # Retrieve the method representing the test
      klass   = @tests[testname]
      object  = @objects[klass]
      method  = object.method(TestPrefix + testname)
      
      # Call the method
      args = []
      args << ns unless ns.nil?
      args << ip unless ip.nil?
      begin
        method.call(*args)
      rescue Dnsruby::ResolvError => e
        return e unless report
        desc = Test::Result::Desc::new(false)
        desc.error = "Resolver error (#{e})"
        @param.report.fatal << Test::Error::new(testname, desc, ns, ip)
      end
    }
  end

  #
  # Perform all the tests as asked in the configuration file and
  # according to the program parameters
  #
  def check
    
    threadlist	= []
    testcount	= 0
    domainname_s	= @param.domain.name.to_s
    starttime	= Time::now
    exception = false
    # Stats
    @param.info.nscount = @param.domain.ns.size
    
    # Do a pre-evaluation of the code
    if @do_preeval
      # Sanity check for debugging
      if $dbg.enabled?(DBG::NOCACHE)
        raise 'Debugging with preeval and NOCACHE is not adviced'
      end
    
      # Do the pre-evaluation
      #  => compute the number of checking to perform
      begin
         Config::TestSeqOrder.each { |family|
        next unless rules = @config.rules[family]
        @iterer[family].call(proc { |*args|
        testcount += rules.preeval(self, args)
         })
        }
      rescue Instruction::InstructionError => e
        $dbg.msg(DBG::TESTS) { "disabling preeval: #{e}" }
        @do_preeval = false
        testcount   = 0
      end
    end

    # Perform the tests
    begin
      # Counter start
      @publisher.progress.start(testcount)
  
      # Perform the checking
       Config::TestSeqOrder.each { |family|
        next unless rules = @config.rules[family]
        threadlist	= []
        @iterer[family].call(proc { |*args|
          threadlist << Thread::new(Thread.current) { |parent|
  			    begin
              rules.eval(self, args)
            rescue Report::FatalError => e
              unless exception
                exception = true
                parent.raise Report::FatalError
              end 
            rescue Exception => e
              # XXX: debuging
              parent.raise e
            end
          }
        })
        threadlist.each { |thr| thr.join }
      }
  
      # Counter final status
      if @param.report.fatal.empty?
      then @publisher.progress.done(domainname_s)
      else @publisher.progress.failed(domainname_s)
      end
    
    rescue Report::FatalError
      threadlist.each {|thread|
        Thread.kill(thread)
      }
      if @param.report.fatal.empty?
        raise "BUG: FatalError with no fatal error stored in report"
      end
      @publisher.progress.failed(domainname_s)
    rescue => e 
      threadlist.each {|thread|
        Thread.kill(thread)
      }
      raise e
    ensure
      # Counter cleanup
      @publisher.progress.finish
      # Total testing time
      @param.info.testingtime = Time::now - starttime
    end
    
    # Status
    @param.report.fatal.empty?
  end
end
end