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