C0 code coverage information
Generated on Mon May 21 22:36:02 -0400 2007 with rcov 0.8.0
Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
1 require 'rools/errors'
2 require 'rools/rule'
3 require 'rools/base'
4 require 'rools/facts'
5 require 'rools/csv_table'
6
7 require 'rexml/document'
8
9 module Rools
10 class RuleSet < Base
11 attr_reader :num_executed, :num_evaluated, :facts, :status
12
13 PASS = :pass
14 FAIL = :fail
15 UNDETERMINED = :undetermined
16
17 # You can pass a set of Rools::Rules with a block parameter,
18 # or you can pass a file-path to evaluate.
19 def initialize(file = nil, &b)
20
21 @rules = {}
22 @facts = {}
23 @dependencies = {}
24
25 if block_given?
26 instance_eval(&b)
27 elsif file
28 # loading a file, check extension
29 name,ext = file.split(".")
30 logger.debug("loading ext: #{name}.#{ext}") if logger
31 case ext
32 when 'csv'
33 load_csv( file )
34
35 when 'xml'
36 load_xml( file )
37
38 when 'rb'
39 load_rb( file )
40
41 when 'rules' # for backwards compatibility
42 load_rb(file)
43
44 else
45 raise "invalid file extension: #{ext}"
46 end
47 end
48 end
49
50 #
51 # Loads decision table
52 #
53 def load_csv( file )
54 csv = CsvTable.new( file )
55 logger.debug "csv rules: #{csv.rules}" if logger
56 instance_eval(csv.rules)
57 end
58
59 #
60 # XML File format loading
61 #
62 def load_xml( fileName )
63 begin
64 file = File.new( fileName )
65 doc = REXML::Document.new file
66 doc.elements.each( "rule-set") { |rs|
67 facts = rs.elements.each( "facts") { |f|
68 facts( f.attributes["name"] ) do f.text.strip end
69 }
70
71 rules = rs.elements.each( "rule") { |rule_node|
72 rule_name = rule_node.attributes["name"]
73 priority = rule_node.attributes["priority"]
74
75 rule = Rule.new(self, rule_name, priority, nil)
76
77 parameters = rule_node.elements.each("parameter") { |param|
78 #logger.debug "xml parameter: #{param.text.strip}"
79 rule.parameters(eval(param.text.strip))
80 }
81
82 conditions = rule_node.elements.each("condition") { |cond|
83 #logger.debug "xml condition #{cond}"
84 rule.condition do eval(cond.text.strip) end
85 }
86
87 consequences = rule_node.elements.each("consequence") { |cons|
88 #logger.debug "xml consequence #{cons}"
89 rule.consequence do eval(cons.text.strip) end
90 }
91
92 @rules[rule_name] = rule
93 }
94 logger.debug( "loaded #{rules.size} rules") if logger
95 }
96 rescue Exception => e
97 puts "Load XML Exception: #{e.to_s}"
98 puts e.backtrace.join("\n")
99 end
100
101 end
102
103 #
104 # Ruby File format
105 #
106 def load_rb( file )
107 instance_eval(File::open(file).read)
108 end
109
110 #
111 # returns an array of facts
112 #
113 def get_facts
114 @facts
115 end
116
117 #
118 # returns all the rules defined for that set
119 #
120 def get_rules
121 @rules
122 end
123
124 # rule creates a Rools::Rule. Make sure to use a descriptive name or symbol.
125 # For the purposes of extending Rules, all names are converted to
126 # strings and downcased.
127 # ==Example
128 # rule 'ruby is the best' do
129 # condition { language.name.downcase == 'ruby' }
130 # consequence { "#{language.name} is the best!" }
131 # end
132 def rule(name, priority=0, &b)
133 name.to_s.downcase!
134 @rules[name] = Rule.new(self, name, priority, b)
135 end
136
137 # facts can be created in a similar manner to rules
138 # all names are converted to strings and downcased.
139 # Facts name is equivalent to a Class Name
140 # ==Example
141 # require 'rools'
142 #
143 # rules = Rools::RuleSet.new do
144 #
145 # facts 'Countries' do
146 # ["China", "USSR", "France", "Great Britain", "USA"]
147 # end
148 #
149 # rule 'Is it on Security Council?' do
150 # parameter String
151 # condition { countries.include?(string) }
152 # consequence { puts "Yes, #{string} is in the country list"}
153 # end
154 # end
155 #
156 # rules.assert 'France'
157 #
158 def facts(name, &b)
159 name.to_s.downcase!
160 @facts[name] = Facts.new(self, name, b)
161 logger.debug( "created facts: #{name}") if logger
162 end
163
164 # A single fact can be an single object of a particular class type
165 # or a collection of objects of a particular type
166 def fact( obj )
167 begin
168 # check if facts already exist for that class
169 # if so, we need to add it to the existing list
170 cls = obj.class.to_s.downcase
171
172 if @facts.key? cls
173 logger.debug( "adding to facts: #{cls}") if logger
174 @facts[cls].fact_value << obj
175 else
176 logger.debug( "creating facts: #{cls}") if logger
177 arr = Array.new
178 arr << obj
179 proc = Proc.new { arr }
180 @facts[cls] = Facts.new(self, cls, proc )
181 end
182 rescue Exception=> e
183 logger.error e if logger
184 end
185 end
186
187 # Delete all existing facts
188 def delete_facts
189 @facts = {}
190 end
191
192 # Use in conjunction with Rools::RuleSet#with to create a Rools::Rule dependent on
193 # another. Dependencies are created through names (converted to
194 # strings and downcased), so lax naming can get you into trouble with
195 # creating dependencies or overwriting rules you didn't mean to.
196 def extend(name, &b)
197 name.to_s.downcase!
198 @extend_rule_name = name
199 instance_eval(&b) if block_given?
200 return self
201 end
202
203 # Used in conjunction with Rools::RuleSet#extend to create a dependent Rools::Rule
204 # ==Example
205 # extend('ruby is the best').with('ruby rules the world') do
206 # condition { language.age > 15 }
207 # consequence { "In the year 2008 Ruby conquered the known universe" }
208 # end
209 def with(name, prio=0, &b)
210 name.to_s.downcase!
211 (@dependencies[@extend_rule_name] ||= []) << Rule.new(self, name, prio, b)
212 #@rules[name] = Rule.new(self, name, prio, b)
213 end
214
215 # Stops the current assertion. Does not indicate failure.
216 def stop(message = nil)
217 @assert = false
218 end
219
220 # Stops the current assertion and change status to :fail
221 def fail(message = nil)
222 @status = FAIL
223 @assert = false
224 end
225
226
227
228 # Used to create a working-set of rules for an object, and evaluate it
229 # against them. Returns a status, simply PASS or FAIL
230 #def assert_1(obj)
231 # @status = PASS
232 # @assert = true
233 # @num_executed = 0;
234 # @num_evaluated = 0;
235
236 # create a working-set of all parameter-matching, non-dependent rules
237 # available_rules = @rules.values.select { |rule| rule.parameters_match?(obj) }
238
239 # available_rules = available_rules.sort do |r1, r2|
240 # r2.priority <=> r1.priority
241 # end
242
243 # begin
244
245 # loop through the available_rules, evaluating each one,
246 # until there are no more matching rules available
247 # begin # loop
248
249 # the loop condition is reset to break by default after every iteration
250 # matches = false
251 #logger.debug("available rules: #{available_rules.size.to_s}") if logger
252 # available_rules.each do |rule|
253 # RuleCheckErrors are caught and swallowed and the rule that
254 # raised the error is removed from the working-set.
255 # logger.debug("evaluating: #{rule}") if logger
256 # begin
257 # @num_evaluated += 1
258 # if rule.conditions_match?(obj)
259 # logger.debug("rule #{rule} matched") if logger
260 # matches = true
261
262 # remove the rule from the working-set so it's not re-evaluated
263 # available_rules.delete(rule)
264
265 # find all parameter-matching dependencies of this rule and
266 # add them to the working-set.
267 # if @dependencies.has_key?(rule.name)
268 # available_rules += @dependencies[rule.name].select do |dependency|
269 # dependency.parameters_match?(obj)
270 # end
271 # end
272
273 # execute this rule
274 # logger.debug("executing rule #{rule}") if logger
275 # rule.call(obj)
276 # @num_executed += 1
277
278 # break the current iteration and start back from the first rule defined.
279 # break
280 # end # if rule.conditions_match?(obj)
281
282 # rescue RuleCheckError => e
283 # log da error or sumpin
284 # available_rules.delete(e.rule)
285 # @status = fail
286 # end # begin/rescue
287
288 # end # available_rules.each
289
290 # end while(matches && @assert)
291
292 # rescue RuleConsequenceError => rce
293 # RuleConsequenceErrors are allowed to break out of the current assertion,
294 # then the inner error is bubbled-up to the asserting code.
295 # @status = fail
296 # raise rce.inner_error
297 # end
298
299 # @assert = false
300
301 # return @status
302 #end # def assert
303
304 # Turn passed object into facts and evaluate all relevant rules
305 # Previous facts of same type are removed
306 def assert( *objs )
307 objs.each { |obj|
308 fact(obj)
309 }
310 return evaluate()
311 end
312
313 # get all relevant rules for all specified facts
314 def get_relevant_rules
315 @relevant_rules = Array.new
316 @facts.each { |k,f|
317 @rules.values.select { |rule|
318 if !@relevant_rules.include?( rule)
319 if rule.parameters_match?(f.value)
320 @relevant_rules << rule
321 logger.debug "#{rule} is relevant" if logger
322 else
323 logger.debug "#{rule} is not relevant" if logger
324 end
325 end
326 }
327 }
328
329 # sort array in rule priority order
330 @relevant_rules = @relevant_rules.sort do |r1, r2|
331 r2.priority <=> r1.priority
332 end
333 end
334
335 # evaluate all relevant rules for specified facts
336 def evaluate
337 @status = PASS
338 @assert = true
339 @num_executed = 0;
340 @num_evaluated = 0;
341
342 get_relevant_rules()
343 logger.debug("no relevant rules") if logger && @relevant_rules.size==0
344
345 begin #rescue
346
347 # loop through the available_rules, evaluating each one,
348 # until there are no more matching rules available
349 begin # loop
350
351 # the loop condition is reset to break by default after every iteration
352 matches = false
353 obj = nil #deprecated
354
355 #logger.debug("available rules: #{available_rules.size.to_s}") if logger
356 @relevant_rules.each do |rule|
357 # RuleCheckErrors are caught and swallowed and the rule that
358 # raised the error is removed from the working-set.
359 logger.debug("evaluating: #{rule}") if logger
360 begin
361 @num_evaluated += 1
362 if rule.conditions_match?(obj)
363 logger.debug("rule #{rule} matched") if logger
364 matches = true
365
366 # remove the rule from the working-set so it's not re-evaluated
367 @relevant_rules.delete(rule)
368
369 # find all parameter-matching dependencies of this rule and
370 # add them to the working-set.
371 if @dependencies.has_key?(rule.name)
372 logger.debug( "found dependant rules to #{rule}") if logger
373 @relevant_rules += @dependencies[rule.name].select do |dependency|
374 dependency.parameters_match?(obj)
375 end
376 end
377
378 # execute this rule
379 logger.debug("executing rule #{rule}") if logger
380 rule.call(obj)
381 @num_executed += 1
382
383 # break the current iteration and start back from the first rule defined.
384 break
385 end # if rule.conditions_match?(obj)
386
387 rescue RuleCheckError => e
388 puts "evaluate RuleCheckError: #{e}"
389 logger.debug( "RuleCheckError") if logger
390 @relevant_rules.delete(e.rule)
391 @status = fail
392 end # begin/rescue
393
394 end # available_rules.each
395
396 end while(matches && @assert)
397
398 rescue RuleConsequenceError => rce
399 # RuleConsequenceErrors are allowed to break out of the current assertion,
400 # then the inner error is bubbled-up to the asserting code.
401 @status = fail
402 raise rce.inner_error
403 end
404
405 @assert = false
406
407 return @status
408 end
409
410 end # class RuleSet
411 end # module Rools
Generated using the rcov code coverage analysis tool for Ruby version 0.8.0.