# Author:: The TTK Team. # Copyright:: Copyright (c) 2004, 2005 TTK team. All rights reserved. # License:: LGPL # $Id: Strategy.rb 575 2005-04-14 10:22:30Z polrop $ require 'set' require 'timeout' require 'synflow' require 'thread_mutex' require 'attributed_class' module TTK # FIXME: explain here how to write a new strategy 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 require 'ttk/strategies/Strategy/assert_eval' 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 # # Exceptions # # Warn: do not raise this exception it's almost deprecated. class Failure < TTKException end # # Accessors # attr_reader :status, :symtbl # # Constructor # # Create a new Strategy with the given optional document. def initialize ( &block ) @status = StartStatus.new @symbols = {} initialize_attributes @save = nil @reject = Set.new block[self] if block_given? end # # Methods # # Runs this test, proceding like this: # - pre_assertion # - prologue # - run_impl # - assertion # - post_assertion # - epilogue def run ( log=@symtbl[:log] ) @log = log initialize_flow_factory if @symtbl[:flow_factory].nil? flow = @symtbl[:flow] flow << :prologue @status = RunningStatus.new @thread = Thread.current aborted = nil saved_path_size = @log.path_size begin begin # Pre assertion pre_assertion_failed = nil begin unless r = pre_assertion() pre_assertion_failed = "Pre assertion failed (#{r.inspect})" end rescue Exception => ex pre_assertion_failed = ex end if pre_assertion_failed @log.new_node(to_s) @symtbl[:flow] << :error skip(pre_assertion_failed) end begin prologue() # @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(flow, msg) begin flow << :begin_run_impl skip_if_cached Timeout.timeout(@timeout, specific_abort) do run_impl() end ensure flow << :end_run_impl end # Post assertion post_assertion_failed = nil begin unless r = post_assertion() post_assertion_failed = "Post assertion failed (#{r.inspect})" end rescue Exception => ex post_assertion_failed = ex end fail(post_assertion_failed) if post_assertion_failed # Assertion assertion() raise_error('no status given') # Forward StatusException rescue StatusException => ex st = ex.status # Put the exception in `aborted' if it dosen't concern me and abort. if st.is_a? TimeoutAbortStatus aborted = ex if st != specific_abort st.message.gsub!('%name', @name.to_s) end raise ex # Convert the Failure exception with `fail' (Warn: almost deprecated). rescue Failure => ex fail(ex) # Catch all others exceptions and wrapped them in an error. rescue Exception => ex raise_error(ex) end # Threat Status exceptions rescue StatusException => ex begin flow << :epilogue @status = ex.status # Call the specific hook (pass_hook, failed_hook...). send(ex.status.hook_name) epilogue() unless pre_assertion_failed 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 @status.to_ttk_log(@log) unless @reject.include?(:status) @status.weight *= @weight (@log.path_size - saved_path_size).times { |i| @log.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 = TTK::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.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]] if me and @symtbl[:cache_proc][self, me[:status]] @symtbl.local.merge!(me[:symtbl]) skip(@wclass.new(me[:status].weight)) end 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 ]) (vars - attrs - exceptions).each do |var| remove_instance_variable var end end def check_assertion ( header, assertion ) bind = {} code = '' case assertion when String return true if assertion.empty? when Hash return true if assertion.empty? bind = assertion[:files] unless assertion[:files].nil? code = assertion[:assert] unless assertion[:assert].nil? when Array return true if assertion.empty? assertion.all? { |a| check_assertion(header, a) } else code = assertion.to_s end @assert_eval.run(code, bind, "(#@name:#{header})") end protected :check_assertion def pre_assertion @assert_eval = AssertEval.new check_assertion('pre-assertion: ', @pre_assertion) end protected :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 @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) unless @reject.include? :name end unless @symtbl[:tester].nil? @log[:tester] = "#{@symtbl[:tester].config[:tester_name]} " + "[#{@symtbl[:tester].config[:tester_type]}] " + "{#{@symtbl[:tester].uri.host}:" + "#{@symtbl[:tester].uri.port}}" end res.to_ttk_log(@log) 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 post_assertion @assert_eval ||= AssertEval.new check_assertion('post-assertions: ', @post_assertion) end protected :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 # FIXME raise_status AbortStatus.new(@symtbl[:flow], message) end # # Build methods # def testify ( aLoader=nil ) self end def to_s @name || @log.path.to_s || super 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 ) STDERR.puts 'unexpected exception' STDERR.puts exc.long_pp end def display_unexpected_synflow_exc ( exc, arg ) STDERR.puts "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 attribute :name, 'test name', :invisible, :mandatory attribute :strategy, 'the strategy class', Class, Strategy, :mandatory 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' do {} end attribute :pre_assertion, 'ruby code to assert environment condition ' + 'which must return true/false', :invisible do [] end attribute :post_assertion, 'ruby code to assert environment condition ' + 'which must return true/false', :invisible do [] end end # class Strategy end # module Strategies end # module TTK