lib/qed/evaluator.rb in qed-2.6.3 vs lib/qed/evaluator.rb in qed-2.7.0

- old
+ new

@@ -1,233 +1,206 @@ module QED require 'qed/scope' - # = Demonstrandum Evaluator + # Demonstrandum Evaluator is responsible for running demo scripts. + # class Evaluator + # Create new Evaluator instance and then run it. + def self.run(demo, options={}) + new(demo, options).run + end + + # Setup new evaluator instance. # - def initialize(script, *observers) - @script = script - @steps = script.steps + # @param [Demo] demo + # The demo to run. + # + # @param [Array] observers + # Objects that respond to observable interface. + # Typically this is just a Reporter instance. + # + def initialize(demo, options={}) + @demo = demo + @steps = demo.steps - #@file = script.file - #@scope = script.scope - #@binding = script.binding - #@advice = script.advice + #@settings = options[:settings] + @applique = options[:applique] # BOOLEAN FLAG - @observers = observers - end + @observers = options[:observers].to_a + @observers += applique_observers - # - def run - advise!(:before_demo, @script) - advise!(:demo, @script) - run_steps - advise!(:after_demo, @script) + @scope = options[:scope] || Scope.new(demo) end + # Collect applique all the signal-based advice and wrap their evaluation + # in observable procedure calls. # - def run_steps #process - @steps.each do |step| - evaluate(step) + def applique_observers + demo = @demo + demo.applique.map do |a| + Proc.new do |type, *args| + proc = a.__signals__[type.to_sym] + @scope.instance_exec(*args, &proc) if proc + end end end + public + + # The Demo being evaluated. # - def evaluate(step) - type = step.type - advise!(:before_step, step) #, @script.file) - advise!("before_#{type}".to_sym, step) #, @script.file) - case type - when :head - evaluate_head(step) - when :desc - evaluate_desc(step) - when :data - evaluate_data(step) - when :code - evaluate_code(step) - else - raise "fatal: unknown #{type}" - end - advise!("after_#{type}".to_sym, step) #, @script.file) - advise!(:after_step, step) #, @script.file) - end + # @return [Demo] + attr :demo + # The observers. # - def evaluate_head(step) - advise!(:head, step) - end + attr :observers + # Run the demo. # - def evaluate_desc(step) - evaluate_links(step) + def run + advise!(:before_demo, @demo) begin - advise!(:desc, step) - advise!(:when, step) # triggers matchers - rescue SystemExit - pass!(step) - #rescue Assertion => exception - # fail!(step, exception) - rescue Exception => exception - if exception.assertion? - fail!(step, exception) - else - error!(step, exception) - end - else - pass!(step) + advise!(:demo, @demo) + run_steps + ensure + advise!(:after_demo, @demo) end end + private + + # Interate over each step and evaluate it. # - def evaluate_data(step) - #advise!(:data, step) - begin - advise!(:data, step) - rescue SystemExit - pass!(step) - #rescue Assertion => exception - # fail!(step, exception) - rescue Exception => exception - if exception.assertion? - fail!(step, exception) - else - error!(step, exception) - end - else - pass!(step) + def run_steps + @steps.each do |step| + evaluate(step) end end + + # Evaluate a step. + # + # @macro [new] step + # + # @param [Step] step + # The step being evaluated. + # + # @return nothing + def evaluate(step) + advise!(:before_step, step) + advise!(:step, step) - # Evaluate a demo step. - def evaluate_code(step) - begin - advise!(:code, step) - @script.evaluate(step.code, step.lineno) - rescue SystemExit - pass!(step) # TODO: skip!(step) - #rescue Assertion => exception - # fail!(step, exception) - rescue Exception => exception - if exception.assertion? - fail!(step, exception) - else - error!(step, exception) - end + evaluate_links(step) unless step.heading? + + if step.assertive? && !@applique + evaluate_test(step) else - pass!(step) + evaluate_applique(step) end + + advise!(:after_step, step) end # TODO: Not sure how to handle loading links in --comment runner mode. - # TODO: Do not think Scope should be reuseud by imported demo. + + # TODO: Should scope be reused by imported demo ? + + # If there are embedded links in the step description than extract + # them and load them in. + # + # @macro step def evaluate_links(step) step.text.scan(/\[qed:\/\/(.*?)\]/) do |match| file = $1 - # relative to demo script - if File.exist?(File.join(@script.directory,file)) - file = File.join(@script.directory,file) + # relative to demo demo + if File.exist?(File.join(@demo.directory,file)) + file = File.join(@demo.directory,file) end - # ruby or another demo - case File.extname(file) - when '.rb' - import!(file) - else - Demo.new(file, :scope=>@script.scope).run + + advise!(:before_import, file) + begin + advise!(:import, file) + case File.extname(file) + when '.rb' + Kernel.eval(File.read(file), @scope.__binding__, file) + else + demo = Demo.new(file) + Evaluator.new(demo, :scope=>@scope).run + end + ensure + advise!(:after_import, file) end end end + # Evaluate step at the *applique level*. This means the execution + # of code and even matcher evaluations will not be captured by a + # rescue clause. # - def pass!(step) - advise!(:pass, step) + # @macro step + def evaluate_applique(step) + advise!(:before_applique, step) + begin + advise!(:applique, step) + evaluate_matchers(step) + evaluate_example(step) + ensure + advise!(:after_applique, step) + end end - # - def fail!(step, exception) - advise!(:fail, step, exception) - #raise exception - end + # Exceptions to always raise regardless. + FORCED_EXCEPTIONS = [NoMemoryError, SignalException, Interrupt] #, SystemExit] + # Evaluate the step's matchaters and code sample, wrapped in a begin-rescue + # clause. # - def error!(step, exception) - advise!(:error, step, exception) - #raise exception - end - - # - def import!(file) - advise!(:unload) # should this also occur just befor after_demo ? - eval(File.read(file), @script.binding, file) - advise!(:load, file) - end - - # Dispatch event to observers and advice. - def advise!(signal, *args) - @observers.each{ |o| o.update(signal, *args) } - - #@script.advise(signal, *args) - case signal - when :when - call_matchers(*args) + # @macro step + def evaluate_test(step) + advise!(:before_test, step) + begin + advise!(:test, step) # name ? + evaluate_matchers(step) + evaluate_example(step) + rescue *FORCED_EXCEPTIONS + raise + rescue SystemExit # TODO: why pass on SystemExit ? + advise!(:pass, step) + #rescue Assertion => exception + # advise!(:fail, step, exception) + rescue Exception => exception + if exception.assertion? + advise!(:fail, step, exception) + else + advise!(:error, step, exception) + end else - call_signals(signal, *args) + advise!(:pass, step) + ensure + advise!(:after_test, step) end end + # Evaluate the step's example in the demo's context, if the example + # is source code. # - #def advise_when!(match) - # @advice.call_when(match) - #end - - # React to an event. - # - # TODO: Should events short circuit on finding first match? - # In other words, should there be only one of each type of signal - # ragardless of how many applique layers? - def call_signals(type, *args) - @script.applique.each do |a| - signals = a.__signals__ - proc = signals[type.to_sym] - #signals.each do |set| - #proc = set[type.to_sym] - #proc.call(*args) if proc - @script.scope.instance_exec(*args, &proc) if proc - #end - end - - #@script.applique.each do |a| - # signals = a.__signals__ - # proc = signals[type.to_sym] - # if proc - # @script.scope.instance_exec(*args, &proc) - # break - # end - #end - - #meth = "qed_#{type}" - #if @script.scope.respond_to?(meth) - # meth = @script.scope.method(meth) - # if meth.arity == 0 - # meth.call - # else - # meth.call(*args) - # end - #end - - #@script.scope.__send__(meth, *args) + # @macro step + def evaluate_example(step) + @scope.evaluate(step.code, step.file, step.lineno) if step.code? end + # Search the step's description for applique matches and + # evaluate them. # - def call_matchers(section) - match = section.text - args = section.arguments - @script.applique.each do |a| - matchers = a.__matchers__ - matchers.each do |(patterns, proc)| + # @macro step + def evaluate_matchers(step) + match = step.text + + @demo.applique.each do |app| + app.__matchers__.each do |(patterns, proc)| compare = match matched = true params = [] patterns.each do |pattern| case pattern @@ -235,57 +208,112 @@ regex = pattern else regex = match_string_to_regexp(pattern) end if md = regex.match(compare) + advise!(:match, step, md) # ADVISE ! params.concat(md[1..-1]) compare = md.post_match else matched = false break end end if matched - params += args - #proc.call(*params) - @script.scope.instance_exec(*params, &proc) + #args = [params, arguments].reject{|e| e == []} # use single argument for params in 3.0? + args = params + args = args + [step.sample_text] if step.data? + args = proc.arity < 0 ? args : args[0,proc.arity] + + #@demo.scope + @scope.instance_exec(*args, &proc) #proc.call(*args) end end end end + # + SPLIT_PATTERNS = [ /(\(\(.*?\)\)(?!\)))/, /(\/\(.*?\)\/)/, /(\/\?.*?\/)/ ] + + # + SPLIT_PATTERN = Regexp.new(SPLIT_PATTERNS.join('|')) + # Convert matching string into a regular expression. If the string # contains double parenthesis, such as ((.*?)), then the text within # them is treated as in regular expression and kept verbatium. # - # TODO: Better way to isolate regexp. Maybe ?:(.*?) or /(.*?)/. - # - # TODO: Now that we can use multi-patterns, do we still need this? - # def match_string_to_regexp(str) - str = str.split(/(\(\(.*?\)\))(?!\))/).map{ |x| - x =~ /\A\(\((.*)\)\)\Z/ ? $1 : Regexp.escape(x) - }.join - str = str.gsub(/\\\s+/, '\s+') + re = nil + str = str.split(SPLIT_PATTERN).map do |x| + case x + when /\A\(\((.*?)\)\)(?!\))/ + $1 + when /\A\/(\(.*?\))\// + $1 + when /\A\/(\?.*?)\// + "(#{$1})" + else + Regexp.escape(x) + end + end.join + + str = str.gsub(/\\\s+/, '\s+') # Replace space with variable space. + Regexp.new(str, Regexp::IGNORECASE) + end - #rexps = [] - #str = str.gsub(/\(\((.*?)\)\)/) do |m| - # rexps << '(' + $1 + ')' - # "\0" - #end - #str = Regexp.escape(str) - #rexps.each do |r| - # str = str.sub("\0", r) - #end - #str = str.gsub(/(\\\ )+/, '\s+') - #Regexp.new(str, Regexp::IGNORECASE) +=begin + # The following code works as well, and can provide a MatchData + # object instead of just matching params, but I call YAGNI on that + # and it has two benefits. 1) the above code is faster, and 2) + # using params allows |(name1, name2)| in rule blocks. + + # + def evaluate_matchers(step) + match = step.text + args = step.arguments + @demo.applique.each do |a| + matchers = a.__matchers__ + matchers.each do |(patterns, proc)| + re = build_matcher_regexp(*patterns) + if md = re.match(match) + #params = [step.text.strip] + params + #proc.call(*params) + @demo.scope.instance_exec(md, *args, &proc) + end + end + end end - ## - #def method_missing(s, *a) - # super(s, *a) unless /^tag/ =~ s.to_s - #end + # + def build_matcher_regexp(*patterns) + parts = [] + patterns.each do |pattern| + case pattern + when Regexp + parts << pattern + else + parts << match_string_to_regexp(pattern) + end + end + Regexp.new(parts.join('.*?'), Regexp::MULTILINE) + end +=end + + # TODO: pass demo to advice? + + # Dispatch an advice event to observers. + # + # @param [Symbol] signal + # The name of the dispatch. + # + # @param [Array<Object>] args + # Any arguments to send along witht =the signal to the observers. + # + # @return nothing + def advise!(signal, *args) + @observers.each{ |o| o.call(signal.to_sym, *args) } + end end end