# Author:: The Uttk Team. # Copyright:: Copyright (c) 2004, 2005 Uttk team. All rights reserved. # License:: LGPL # $Id: /w/fey/uttk/trunk/lib/uttk/strategies/Strategy.rb 22184 2006-02-23T16:12:25.225774Z pouillar $ require 'set' require 'timeout' module Uttk # A tutorial introduces how to create new strategies at # http://uttk.org/shelf/documentation module Strategies # This is the base class for anything _testable_. It provides some basics # attributes like the name of the test. class Strategy include Abstract include AttributedClass class AttributedClass::Attribute alias :initialize_without_expand :initialize def initialize ( name, desc, *a, &b ) my, our = a.partition { |x| x == :dont_expand } @expand = my.empty? initialize_without_expand(name, desc, *our, &b) end def expand? @expand end end # # Accessors # attr_reader :status # # Constructor # # Create a new Strategy with the given optional document. def initialize ( name=nil, &block ) @status = StartStatus.new @symbols = {} @symtbl = nil initialize_attributes @save = nil @reject = Set.new self.name = name if block if block.arity == -1 instance_eval(&block) else block[self] end end end module UttkTimeout module_function def timeout(sec, flow, exception=Error, &block) return block[] if sec == nil or sec.zero? begin x = Thread.current y = Thread.start do sleep sec if x.alive? flow << :abort x.raise exception, "execution expired" end end block[sec] ensure y.kill if y and y.alive? end end end # module UttkTimeout # # Methods # # Runs this test, proceding like this: # - check_pre_assertion # - prologue # - run_impl # - assertion # - check_post_assertion # - epilogue def run ( log=@symtbl[:log] ) unless @status.is_a? StartStatus raise_error("This test was already run (#{self} : #{self.class})") end @log = log @benchmark = [] initialize_flow_factory if @symtbl[:flow_factory].nil? flow = @symtbl[:flow] flow << :prologue @status = RunningStatus.new @thread = Thread.current aborted = nil weight_copy = nil @upper = @log.upper begin begin # Pre assertion pre_assertion_failed = nil begin unless r = check_pre_assertion() pre_assertion_failed = "Pre assertion failed (#{r.inspect})" end rescue Exception => ex pre_assertion_failed = "Pre assertion failed (#{ex})" end if pre_assertion_failed @log.new_node(to_s) @symtbl[:flow] << :error skip(pre_assertion_failed) end begin @benchmark << Benchmark.measure('prologue') do prologue() weight_copy = @weight end # @log.flow_id = flow.i # Catch all other exceptions and wrapped them in an error. rescue Exception => ex raise_error(ex) end msg = "abort '%name' with timeout #{@timeout}s" specific_abort = TimeoutAbortStatus.new(msg) begin flow << :begin_run_impl skip_if_cached skip_wrt_rpath_and_rpath_exclude UttkTimeout.timeout(@timeout, flow, specific_abort) do @benchmark << Benchmark.measure('run') do run_impl() end end ensure flow << :end_run_impl end # Post assertion post_assertion_failed = nil begin unless r = check_post_assertion() post_assertion_failed = "Post assertion failed (#{r.inspect})" end rescue Exception => ex post_assertion_failed = "Post assertion failed (#{ex})" end fail(post_assertion_failed) if post_assertion_failed # Assertion assertion() raise_error('no status given') # Forward StatusException rescue Status => ex # Put the exception in `aborted' if it dosen't concern me and abort. if ex.is_a? TimeoutAbortStatus aborted = ex if ex != specific_abort ex.reason.gsub!('%name', @name.to_s) end raise ex # Catch all others exceptions and wrapped them in an error. rescue Exception => ex raise_error(ex) end # Threat Status exceptions rescue Status => ex begin flow << :epilogue @status = ex # Call the specific hook (pass_hook, failed_hook...). send(ex.hook_name) unless pre_assertion_failed @benchmark << Benchmark.measure('epilogue') do epilogue() end end rescue SynFlow::Error => ex display_unexpected_synflow_exc ex, :epilogue rescue Exception => ex display_unexpected_exc ex end rescue Exception => ex display_unexpected_exc ex end begin if @symtbl[:benchmark] and (not @reject.include?(:benchmark)) @log.new_node(:benchmark, :ordered => true) do @benchmark.each { |b| @log << b } end end @log << status unless @reject.include?(:status) @status.weight *= weight_copy @upper.up if cache = @symtbl[:cache] cache[@symtbl[:pathname]] = { :status => @status, :symtbl => @symtbl.local } end @reject.clear if defined? @reject clean_instance_variables flow << :finish rescue SynFlow::Error => ex display_unexpected_synflow_exc ex, :finish rescue Exception => ex display_unexpected_exc ex end raise aborted unless aborted.nil? return @status end alias call run # # Custom accessors # def wclass=(other) if other.is_a?(Class) @wclass = other else @wclass = Uttk::Weights.const_get(other.to_s) end end # # Assign # # Assign an attribute safely from a key:String and a value. def assign_one(key, val, silent=false) return if [:strategy].include? key.to_sym meth = key.to_s + '=' if !val.nil? if respond_to?(meth) or respond_to?(:method_missing) send(meth, val) elsif not silent @log.warn { "Attribute #{key} not found for #{self.class}" } end end end protected :assign_one # This can be overrided to specify which attribute is assign at last. def assign_at_last nil end protected :assign_at_last # Assign a +hsh+ keeping at last the attribute referenced by # _assign_at_last_. def assign(hsh) last = assign_at_last hsh.each_pair { |key, val| assign_one(key, val) if key.to_sym != last } assign_one(last, hsh[last]) if hsh.has_key? last end # # Reject # def reject ( *att ) @reject += att.map! { |a| a.to_sym } end def initialize_flow_factory factory = SynFlowFactory.new factory << { :start_st => { :prologue => :prologue_st, :not_running? => :start_st, :end_run_impl => :after_run_st, # :abort => :start_st, :error => :error_st }, :prologue_st => { :begin_run_impl => :run_impl_st, :error => :error_st }, :run_impl_st => { :prologue => :prologue_st, :end_run_impl => :after_run_st, :abort => :abort_st, :error => :error_st }, :after_run_st => { :epilogue => :epilogue_st, :error => :error_st }, :epilogue_st => { :finish => :start_st, :error => :error_st }, :abort_st => { :end_run_impl => :after_run_abort_st, :abort => :abort_st, :error => :error_st }, :after_run_abort_st => { :epilogue => :epilogue_abort_st, :abort => :after_run_abort_st, :error => :error_st }, :epilogue_abort_st => { :finish => :start_st, :error => :error_st }, :error_st => { :prologue => :prologue_st, :begin_run_impl => :run_impl_st, :end_run_impl => :after_run_st, :epilogue => :epilogue_st, :finish => :start_st, :no_running? => :error_st, :error => :error_st, :abort => :abort_st }, } factory.initial = :start_st @symtbl[:flow_factory] = factory @symtbl[:flow] = factory.new_flow end def skip_if_cached if cache = @symtbl[:use_cache] me = cache[@symtbl[:pathname].to_sym] if me and @symtbl[:cache_proc][self, me[:status]] @symtbl.local.merge!(me[:symtbl]) skip(@wclass.new(me[:status].weight)) end end end def skip_wrt_rpath_and_rpath_exclude re = @symtbl[:rpath] re_ex = @symtbl[:rpath_exclude] lpath = @log.path if re and not lpath.lpath_prefix re skip(@wclass.new(:PASS)) end if re_ex and lpath.rpath_prefix re_ex skip(@wclass.new(:PASS)) end end def clean_instance_variables attrs = Set.new(self.class.attributes.map { |a| "@#{a.name}" }) vars = Set.new(instance_variables) exceptions = Set.new(%w[ @status @thread @symtbl ]) (vars - attrs - exceptions).each do |var| remove_instance_variable var end end def check_assertion ( header, assertion ) if assertion.is_a?(Proc) instance_eval(&assertion) else instance_eval(assertion.to_s) end end protected :check_assertion def check_pre_assertion check_assertion('pre-assertion', @pre_assertion) end protected :check_pre_assertion # Display test attributes. Attributes displayed and their order is # specified by the class attribute _attributes_. def prologue if @symtbl[:loader].nil? raise ArgumentError, 'no loader in the symtbl' end @save = {} res = OHash.new check_attributes each_attribute do |attr, val| next if val.nil? name = attr.name if attr.expand? and new_val = val.symtbl_gsub(@symtbl) @save[name] = val val = new_val send("#{name}=", val) end if attr.visible? and not @reject.include? name res[name] = val unless val == attr.default or name == :name end end @symbols.each do |k, v| @symtbl[k] = v end unless self.name.nil? or @reject.include? :name if @symtbl[:pathname].nil? @symtbl[:pathname] = ('/' + self.name).to_sym else @symtbl[:pathname] = :"#{@symtbl[:pathname]}/#{self.name}" end @log.new_node(self.name, :type => self.class) end @log << res end protected :prologue # Compute really the test, only this method can be time checked. def run_impl end protected :run_impl # Exploit the results and assert when you want def assertion end protected :assertion def check_post_assertion check_assertion('post-assertions', @post_assertion) end protected :check_post_assertion # Display conclusions. # Here you can finish some tasks even when the test fails. # Exceptions are absolutly forbidden in this method. def epilogue unless @save.nil? @save.each { |k,v| send("#{k}=", v) } @save.clear end end protected :epilogue # Here you can do some tasks when the test failed. def failed_hook end protected :failed_hook # Here you can do some tasks when an error occur during the test. def error_hook end protected :error_hook # Here you can do some tasks when the test is aborted. def abort_hook end protected :abort_hook # Here you can do some task when the test is skiped. def skip_hook end protected :skip_hook # Here you can do some tasks when the test is passed. def pass_hook end protected :pass_hook # # Assertions # def raise_status ( status ) @thread.raise(status) end protected :raise_status def raise_status_custom ( status_class, *args ) weight, message = args.partition { |x| x.is_a? Weights::Weight } if weight.size > 1 or message.size > 1 raise ArgumentError, 'too much arguments' end raise_status status_class.new(weight[0], message[0]) end protected :raise_status_custom # Force the test to pass def pass raise_status PassStatus.new end # Skip the test def skip ( *args ) raise_status_custom SkipStatus, *args end # Force an Error status def raise_error ( message=nil ) @symtbl[:flow] << :error raise_status ErrorStatus.new(message) end # Force the test to fail def fail ( *args ) raise_status_custom FailStatus, *args end # Abort the test explicitly. def abort ( message='abort explicitly' ) @symtbl[:flow] << :abort raise_status AbortStatus.new(message) end # # Build methods # def testify ( symtbl, &block ) test = dup test.symtbl ||= symtbl block[test] if block test end def to_s @name || @log.path.to_s || super end def name= ( anObject ) case anObject when nil, '' @name = nil else @name = anObject.to_s end end def timeout=(other) if other >= 0 @timeout = other else raise(ArgumentError, "`#{other}' - timeout delay must be >= 0") end end def running? defined? @status and @status.is_a? RunningStatus end def display_unexpected_exc ( exc ) @log.error_unexpected_exception = exc end def display_unexpected_synflow_exc ( exc, arg ) @log.error_unexpected_synflow_exception = arg end # FIXME: I'm not well dumped def symbols= ( symbols ) symbols.each do |k, v| @symbols[k] = v end end def strategy self.class end def strategy= ( aClass ) if aClass != Strategy and aClass != self.class raise ArgumentError, "Cannot change the strategy class " + "of a test (#{aClass} != #{self.class})" end end def symtbl= ( aSymtbl ) @symtbl = aSymtbl @log ||= @symtbl[:log] end def symtbl (aSymtbl=nil) if aSymtbl.nil? @symtbl else self.symtbl = aSymtbl end end def self.to_yaml_type name.sub(/^Uttk::Strategies/, '!S') end def self.to_form ( b ) b.form_tag do b.table :class => 'uttk_attributes' do attributes.each do |attribute| b.tr :class => 'uttk_attribute' do b.td(:class => 'uttk_attribute_description') do b.itext! "#{attribute.name} (#{attribute.descr}): " end b.td(:class => 'uttk_attribute_field') do attribute.to_form(b) end end end end end end attribute :name, 'test name', :invisible, String attribute :strategy, 'the strategy class', Class, Strategy, :mandatory, :invisible attribute :wclass, 'a class to control weights', Class, Weights::Default attribute :weight, 'a sort of coefficient. See wclass', 1 attribute :fatal, 'if the test fail all the suite fail', false attribute :timeout, 'a duration (in seconds) before failure', 0 attribute :symbols, 'some user defined symbols', Hash do {} end assertions_classes = [String, Proc, TrueClass, FalseClass] attribute :pre_assertion, 'ruby code to assert environment condition ' + 'which must return true/false', :invisible, assertions_classes do true end attribute :post_assertion, 'ruby code to assert environment condition ' + 'which must return true/false', :invisible, assertions_classes do true end end # class Strategy each_module_name do |name| YAML.add_builtin_type("S::#{name}") do |type, val| unless val.is_a? Hash raise ArgumentError, "#{type}: needs a Hash not a #{val.class} (#{val.inspect})" end if val.has_key? :strategy and val[:strategy].to_s != to_s raise ArgumentError, "type conflict #{type} != #{val[:strategy]}" end val[:strategy] = Strategies.const_get(type.sub(/^.*S::/, '')) val end end end # module Strategies end # module Uttk