scrubyt 0.1.0 → 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +34 -0
- data/COPYING +340 -0
- data/README +34 -5
- data/Rakefile +6 -5
- data/lib/scrubyt.rb +1 -0
- data/lib/scrubyt/constraint.rb +12 -24
- data/lib/scrubyt/constraint_adder.rb +3 -17
- data/lib/scrubyt/export.rb +33 -17
- data/lib/scrubyt/extractor.rb +74 -23
- data/lib/scrubyt/filter.rb +52 -37
- data/lib/scrubyt/pattern.rb +74 -30
- data/lib/scrubyt/post_processor.rb +58 -0
- data/lib/scrubyt/result.rb +2 -2
- data/lib/scrubyt/result_dumper.rb +6 -0
- data/lib/scrubyt/xpathutils.rb +52 -15
- data/test/unittests/constraint_test.rb +0 -3
- data/test/unittests/extractor_test.rb +11 -13
- data/test/unittests/xpathutils_test.rb +31 -31
- metadata +8 -5
data/lib/scrubyt/pattern.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'hpricot'
|
3
|
-
require 'open-uri'
|
4
3
|
|
5
4
|
module Scrubyt
|
6
5
|
##
|
@@ -43,7 +42,8 @@ module Scrubyt
|
|
43
42
|
|
44
43
|
attr_accessor :name, :output_type, :generalize, :children, :filters, :parent,
|
45
44
|
:last_result, :result, :root_pattern, :example, :block_count,
|
46
|
-
:next_page, :limit, :extractor, :extracted_docs,
|
45
|
+
:next_page, :limit, :extractor, :extracted_docs,
|
46
|
+
:examples, :parent_of_leaf
|
47
47
|
attr_reader :type, :generalize_set, :next_page_url
|
48
48
|
|
49
49
|
def initialize (name, *args)
|
@@ -52,12 +52,17 @@ module Scrubyt
|
|
52
52
|
@root_pattern = nil #root pattern of the wrapper
|
53
53
|
@children = [] #child patterns
|
54
54
|
@filters = [] #filters of the wrapper
|
55
|
-
@sink = [] #output of a pattern
|
56
|
-
@source = [] #input of a pattern
|
57
55
|
@result = Result.new #hierarchical results of the pattern
|
58
56
|
@@instance_count = Hash.new(0)
|
57
|
+
@evaluated_examples = []
|
59
58
|
@next_page = nil
|
60
|
-
|
59
|
+
if @examples == nil
|
60
|
+
filters << Scrubyt::Filter.new(self) #create a default filter
|
61
|
+
else
|
62
|
+
@examples.each do |example|
|
63
|
+
filters << Scrubyt::Filter.new(self,example) #create a filter
|
64
|
+
end
|
65
|
+
end
|
61
66
|
end
|
62
67
|
|
63
68
|
#Parse the args passed as *args; There is only one compulsory parameter to pattern: it's name
|
@@ -66,10 +71,8 @@ module Scrubyt
|
|
66
71
|
#If an example is specified, it *MUST* be the first parameter; the order of the other
|
67
72
|
#parameters is irrelevant
|
68
73
|
def parse_args(args)
|
69
|
-
#
|
70
|
-
|
71
|
-
@example = args.delete_at(0) if args[0].instance_of? String
|
72
|
-
@example = args.delete_at(0) if args[0].instance_of? Regexp
|
74
|
+
#Grab any examples that are defined!
|
75
|
+
look_for_examples(args)
|
73
76
|
args.each do |arg|
|
74
77
|
arg.each do |k,v|
|
75
78
|
#Set only the setable fields
|
@@ -96,7 +99,7 @@ module Scrubyt
|
|
96
99
|
#This flag indicates that the user set 'generalize' to some value;
|
97
100
|
#This way we can ensure that the explicit setting will not be overridden
|
98
101
|
@generalize_set ||= false
|
99
|
-
end
|
102
|
+
end
|
100
103
|
|
101
104
|
#Dispatcher function; The class was already too big so I have decided to factor
|
102
105
|
#out some methods based on their functionality (like output, adding constraints)
|
@@ -162,7 +165,7 @@ module Scrubyt
|
|
162
165
|
temp_document = generate_next_page_link(@next_page)
|
163
166
|
return nil if temp_document == nil
|
164
167
|
clear_sources_and_sinks(@root_pattern)
|
165
|
-
@root_pattern.extractor.fetch(temp_document
|
168
|
+
@root_pattern.extractor.fetch(temp_document)
|
166
169
|
attach_current_document
|
167
170
|
end
|
168
171
|
|
@@ -171,17 +174,18 @@ module Scrubyt
|
|
171
174
|
#crawling to a new page
|
172
175
|
def attach_current_document
|
173
176
|
doc = @root_pattern.extractor.get_hpricot_doc
|
174
|
-
|
175
|
-
|
177
|
+
filters[0].source << doc
|
178
|
+
filters[0].sink << doc
|
176
179
|
@last_result ||= []
|
177
180
|
@last_result << doc
|
178
|
-
@result.add_result(
|
181
|
+
@result.add_result(filters[0].source, filters[0].sink)
|
179
182
|
end
|
180
183
|
|
181
184
|
##
|
182
185
|
#Based on the given examples, calculate the XPaths for the tree patterns
|
183
186
|
def setup_examples
|
184
187
|
get_root_pattern(self)
|
188
|
+
mark_leaf_parents(self)
|
185
189
|
set_root_pattern_whole_wrapper(@root_pattern, @root_pattern)
|
186
190
|
generate_examples(@root_pattern)
|
187
191
|
end
|
@@ -192,10 +196,14 @@ module Scrubyt
|
|
192
196
|
def evaluate
|
193
197
|
#No need to evaluate if there is no parent pattern
|
194
198
|
return if @parent == nil
|
195
|
-
|
196
|
-
@
|
197
|
-
@filters.
|
199
|
+
all_filter_results = []
|
200
|
+
@filters.each do |filter|
|
201
|
+
filter_index = @filters.index(filter)
|
202
|
+
filter_index = 0 if @parent.filters.size <= filter_index
|
203
|
+
filter.source = @parent.filters[filter_index].sink
|
204
|
+
filter.source.each do |source|
|
198
205
|
r = filter.evaluate(source)
|
206
|
+
next if r == nil
|
199
207
|
if filter.constraints.size > 0
|
200
208
|
#in the beginning, keys of result_hash are made up of all the results of the filter
|
201
209
|
#with value = true; Later on, only those results will have 'true' value which are
|
@@ -206,24 +214,51 @@ module Scrubyt
|
|
206
214
|
filter.constraints.each { |constraint| result_hash[res] &&= constraint.check(res) }
|
207
215
|
end
|
208
216
|
result = result_hash.reject {|k,v| k if !v}
|
209
|
-
sorted_result = r.reject {|e| !result.keys.include? e}
|
210
|
-
add_result(source, sorted_result)
|
217
|
+
sorted_result = r.reject {|e| !result.keys.include? e}
|
218
|
+
add_result(filter, source, sorted_result)
|
211
219
|
else
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
220
|
+
if ( (xe = @result.lookup(source)) != nil )
|
221
|
+
#puts "ha"; p xe
|
222
|
+
end
|
223
|
+
add_result(filter, source, r)
|
224
|
+
end#end of constraint check
|
225
|
+
end#end of source iteration
|
226
|
+
end#end of filter iteration
|
217
227
|
end
|
218
228
|
|
219
229
|
def get_instance_count
|
220
230
|
@@instance_count
|
221
231
|
end
|
232
|
+
|
233
|
+
def get_constraints
|
234
|
+
filters[0].constraints
|
235
|
+
end
|
222
236
|
|
223
237
|
private
|
224
|
-
def
|
238
|
+
def look_for_examples(args)
|
239
|
+
if (args[0].is_a? String)
|
240
|
+
@examples = args.select {|e| e.is_a? String}
|
241
|
+
#Check if all the String parameters are really the first
|
242
|
+
#parameters
|
243
|
+
args[0..@examples.size-1].each do |example|
|
244
|
+
if !example.is_a? String
|
245
|
+
puts 'FATAL: Problem with example specification'
|
246
|
+
end
|
247
|
+
end
|
248
|
+
elsif (args[0].is_a? Regexp)
|
249
|
+
#Check if all the String parameters are really the first
|
250
|
+
#parameters
|
251
|
+
args[0..@examples.size].each do |example|
|
252
|
+
if !example.is_a? Regexp
|
253
|
+
puts 'FATAL: Problem with example specification'
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def add_result(filter, source, results)
|
225
260
|
results.each do |res|
|
226
|
-
|
261
|
+
filter.sink << res
|
227
262
|
@result.add_result(source, res)
|
228
263
|
@@instance_count[@name] += 1
|
229
264
|
end
|
@@ -238,6 +273,13 @@ private
|
|
238
273
|
end
|
239
274
|
end
|
240
275
|
|
276
|
+
def mark_leaf_parents(pattern)
|
277
|
+
pattern.children.each { |child|
|
278
|
+
pattern.parent_of_leaf = true if child.children.size == 0
|
279
|
+
}
|
280
|
+
pattern.children.each { |child| mark_leaf_parents(child) }
|
281
|
+
end
|
282
|
+
|
241
283
|
def set_root_pattern_whole_wrapper(pattern, root_pattern)
|
242
284
|
pattern.children.each {|child| set_root_pattern_whole_wrapper(child, root_pattern)}
|
243
285
|
pattern.root_pattern = root_pattern
|
@@ -249,15 +291,17 @@ private
|
|
249
291
|
end
|
250
292
|
|
251
293
|
def clear_sources_and_sinks(pattern)
|
252
|
-
pattern.
|
253
|
-
|
294
|
+
pattern.filters.each do |filter|
|
295
|
+
filter.source = []
|
296
|
+
filter.sink = []
|
297
|
+
end
|
254
298
|
pattern.children.each {|child| clear_sources_and_sinks child}
|
255
299
|
end
|
256
300
|
|
257
301
|
def generate_next_page_link(example)
|
258
|
-
node = XPathUtils.find_node_from_text(@root_pattern.source[0], example)
|
302
|
+
node = XPathUtils.find_node_from_text(@root_pattern.filters[0].source[0], example)
|
259
303
|
return nil if node == nil
|
260
|
-
node.attributes['href']
|
304
|
+
node.attributes['href'].gsub('&') {'&'}
|
261
305
|
end # end of method generate_next_page_link
|
262
306
|
end #end of class Pattern
|
263
307
|
end #end of module Scrubyt
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Scrubyt
|
2
|
+
##
|
3
|
+
#=<tt>Post processing results after the extraction</tt>
|
4
|
+
#Some things can not be carried out during evaluation - for example
|
5
|
+
#the ensure_presence_of_pattern constraint (since the evaluation is top
|
6
|
+
#to bottom, at a given point we don't know yet whether the currently
|
7
|
+
#evaluated pattern will have a child pattern or not) or removing unneeded
|
8
|
+
#results caused by evaluating multiple filters.
|
9
|
+
#
|
10
|
+
#The sole purpose of this class is to execute these post-processing tasks.
|
11
|
+
class PostProcessor
|
12
|
+
##
|
13
|
+
#Remove unneeded results of a pattern (caused by evaluating multiple filters)
|
14
|
+
#See for example the B&N scenario - the book titles are extracted two times
|
15
|
+
#for every pattern (since both examples generate the same XPath for them)
|
16
|
+
#but since always only one of the results has a price, the other is discarded
|
17
|
+
def self.remove_multiple_filter_duplicates(pattern)
|
18
|
+
remove_multiple_filter_duplicates_intern(pattern) if pattern.parent_of_leaf
|
19
|
+
pattern.children.each {|child| remove_multiple_filter_duplicates(child)}
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def self.remove_multiple_filter_duplicates_intern(pattern)
|
24
|
+
possible_duplicates = {}
|
25
|
+
longest_result = 0
|
26
|
+
pattern.result.childmap.each { |r|
|
27
|
+
r.each do |k,v|
|
28
|
+
v.each do |x|
|
29
|
+
all_child_results = []
|
30
|
+
pattern.children.each { |child|
|
31
|
+
temp_res = child.result.lookup(x)
|
32
|
+
all_child_results << temp_res if temp_res != nil
|
33
|
+
}
|
34
|
+
next if all_child_results.size <= 1
|
35
|
+
longest_result = all_child_results.map {|e| e.size}.max
|
36
|
+
all_child_results.each { |r| (r.size+1).upto(longest_result) { r << nil } }
|
37
|
+
possible_duplicates[x] = all_child_results.transpose
|
38
|
+
end
|
39
|
+
end
|
40
|
+
}
|
41
|
+
#Determine the 'real' duplicates
|
42
|
+
real_duplicates = {}
|
43
|
+
possible_duplicates.each { |k,v|
|
44
|
+
next if v.size == 1
|
45
|
+
v.each { |r| real_duplicates[k] = r }
|
46
|
+
}
|
47
|
+
|
48
|
+
#Finally, remove them!
|
49
|
+
pattern.children.each { |child|
|
50
|
+
child.result.childmap.each { |r|
|
51
|
+
r.each { |k,v|
|
52
|
+
real_duplicates[k].each {|e| v.delete e} if real_duplicates.keys.include? k
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/scrubyt/result.rb
CHANGED
@@ -11,7 +11,7 @@ module Scrubyt
|
|
11
11
|
def add_result(source, result)
|
12
12
|
@childmap.each do |hash|
|
13
13
|
if hash.keys[0] == source
|
14
|
-
hash[source] << result
|
14
|
+
hash[source] << result if !hash[source].include? result
|
15
15
|
return
|
16
16
|
end
|
17
17
|
end
|
@@ -35,7 +35,7 @@ end#end of module Scrubyt
|
|
35
35
|
|
36
36
|
#table
|
37
37
|
# source: doc1
|
38
|
-
# childmap [ {doc1 => [table[1]s1, table[2]s1, table[3]s1]}, doc2 => [table[1]s2, table[2]s2, table[3]s2] ]
|
38
|
+
# childmap [ {doc1 => [table[1]s1, table[2]s1, table[3]s1]}, {doc2 => [table[1]s2, table[2]s2, table[3]s2]} ]
|
39
39
|
|
40
40
|
#row
|
41
41
|
# source: table1s1, table2s1, table3s1
|
@@ -15,9 +15,15 @@ module Scrubyt
|
|
15
15
|
pattern.last_result = lr
|
16
16
|
to_xml_recursive(pattern, root)
|
17
17
|
end
|
18
|
+
remove_empty_leaves(doc)
|
18
19
|
doc
|
19
20
|
end
|
20
21
|
|
22
|
+
def self.remove_empty_leaves(node)
|
23
|
+
node.remove if node.elements.empty? && node.text == nil
|
24
|
+
node.elements.each {|child| remove_empty_leaves child }
|
25
|
+
end
|
26
|
+
|
21
27
|
##
|
22
28
|
#Output the text of the pattern; If this pattern is a tree, collect the text from its
|
23
29
|
#result instance node; otherwise rely on the last_result
|
data/lib/scrubyt/xpathutils.rb
CHANGED
@@ -7,6 +7,13 @@ module Scrubyt
|
|
7
7
|
class XPathUtils
|
8
8
|
#When looking up examples, do NOT recurse into these tags since they won't contain any usable info
|
9
9
|
NON_CONTENT_TAGS = ['form','option', 'input', 'script', 'noscript']
|
10
|
+
ENTITIES = {
|
11
|
+
'quot' => '"',
|
12
|
+
'apos' => "'",
|
13
|
+
'amp' => '&',
|
14
|
+
'lt' => '<',
|
15
|
+
'gt' => '>',
|
16
|
+
'nbsp' => ' '}
|
10
17
|
|
11
18
|
#From the example text defined by the user, find the lowest possible node with the text 'text'.
|
12
19
|
#The text can be also a mixed content text, e.g.
|
@@ -17,14 +24,23 @@ module Scrubyt
|
|
17
24
|
def self.find_node_from_text(doc, text)
|
18
25
|
@node = nil
|
19
26
|
@found = false
|
20
|
-
self.
|
21
|
-
self.lowest_possible_node_with_text(@node, text)
|
27
|
+
self.traverse_for_full_text(doc,text)
|
28
|
+
self.lowest_possible_node_with_text(@node, text) if @node != nil
|
22
29
|
#$Logger.warn("Node for example #{text} Not found!") if (@found == false)
|
23
|
-
|
30
|
+
if (@found == false)
|
31
|
+
#Fallback to per node text lookup
|
32
|
+
self.traverse_for_node_text(doc,text)
|
33
|
+
if (@found == false)
|
34
|
+
puts "FATAL: Node for example #{text} Not found!"
|
35
|
+
puts "Please make sure your specified the example properly"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
p @node
|
24
39
|
@node
|
25
40
|
end
|
26
41
|
|
27
|
-
#Full text of the node; this is equivalent to Hpricot's inner_text
|
42
|
+
#Full text of the node; this is equivalent to Hpricot's inner_text
|
43
|
+
#(? be sure to check). Will be
|
28
44
|
#replaced if Hpricot 0.5 will be released
|
29
45
|
def self.full_text(node)
|
30
46
|
result = ""
|
@@ -119,7 +135,7 @@ module Scrubyt
|
|
119
135
|
#_index_ - there might be more images with the same src on the page -
|
120
136
|
#most typically the user will need the 0th - but if this is not the
|
121
137
|
#case, there is the possibility to override this
|
122
|
-
def self.find_image(doc, example, index=
|
138
|
+
def self.find_image(doc, example, index=1)
|
123
139
|
(doc/"img[@src='#{example}']")[index]
|
124
140
|
end
|
125
141
|
|
@@ -150,7 +166,7 @@ private
|
|
150
166
|
#Note that in classic XPath, the indices start with 1 (rather
|
151
167
|
#than 0).
|
152
168
|
def self.find_index(node)
|
153
|
-
c =
|
169
|
+
c = 0
|
154
170
|
node.parent.children.each do |child|
|
155
171
|
if child.class == Hpricot::Elem
|
156
172
|
c += 1 if (child.name == node.name)
|
@@ -170,27 +186,48 @@ private
|
|
170
186
|
path
|
171
187
|
end
|
172
188
|
|
173
|
-
def self.
|
189
|
+
def self.traverse_for_node_text(node, text)
|
174
190
|
return if @found
|
175
191
|
if (node.instance_of? Hpricot::Elem)
|
176
|
-
|
177
|
-
|
178
|
-
|
192
|
+
node.traverse_text do |t|
|
193
|
+
if (t.to_s == text)
|
194
|
+
@found = true
|
195
|
+
@node = t.parent
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
node.children.each do |child|
|
200
|
+
if child.instance_of? Hpricot::Elem
|
201
|
+
traverse_for_node_text(child, text) unless NON_CONTENT_TAGS.include? child.name
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def self.traverse_for_full_text(node, text)
|
207
|
+
return if @found
|
208
|
+
if (node.instance_of? Hpricot::Elem)
|
209
|
+
ft = unescape_entities(full_text(node)).strip
|
210
|
+
if (ft == text)
|
211
|
+
@found = true
|
212
|
+
@node = node
|
213
|
+
end
|
179
214
|
end
|
180
215
|
node.children.each do |child|
|
181
|
-
traverse_nodes child if child.instance_of? Hpricot::Doc
|
182
216
|
if child.instance_of? Hpricot::Elem
|
183
|
-
|
217
|
+
traverse_for_full_text(child, text) unless NON_CONTENT_TAGS.include? child.name
|
184
218
|
end
|
185
219
|
end
|
186
220
|
end
|
221
|
+
|
222
|
+
def self.unescape_entities(text)
|
223
|
+
ENTITIES.each {|e,s| text.gsub!(/\&#{e};/) {"#{s}"} }
|
224
|
+
text
|
225
|
+
end
|
187
226
|
|
188
227
|
def self.lowest_possible_node_with_text(node, text)
|
189
228
|
return if node.instance_of? Hpricot::Text
|
190
229
|
@node = node if full_text(node) == text
|
191
|
-
node.children.each
|
192
|
-
lowest_possible_node_with_text(child, text)
|
193
|
-
end
|
230
|
+
node.children.each { |child| lowest_possible_node_with_text(child, text) }
|
194
231
|
end #End of method lowest_possible_node_with_text
|
195
232
|
end #End of class XPathUtils
|
196
233
|
end #End of module Scrubyt
|
@@ -1,6 +1,3 @@
|
|
1
|
-
#require File.join(File.dirname(__FILE__), '../..', 'lib', 'constraint')
|
2
|
-
#require File.join(File.dirname(__FILE__), '../..', 'lib', 'extractor')
|
3
|
-
#require File.join(File.dirname(__FILE__), '../..', 'lib', 'constraint_adder')
|
4
1
|
require 'scrubyt'
|
5
2
|
require 'test/unit'
|
6
3
|
|
@@ -1,5 +1,3 @@
|
|
1
|
-
#require File.join(File.dirname(__FILE__), '../../lib', 'extractor.rb')
|
2
|
-
#require File.join(File.dirname(__FILE__), '../../lib', 'pattern.rb')
|
3
1
|
require 'scrubyt'
|
4
2
|
require 'test/unit'
|
5
3
|
|
@@ -7,7 +5,7 @@ class ExtractorTest < Test::Unit::TestCase
|
|
7
5
|
def test_create_one_pattern
|
8
6
|
pattern = Scrubyt::Extractor.define do
|
9
7
|
fetch File.join(File.dirname(__FILE__), "input/test.html")
|
10
|
-
pattern "
|
8
|
+
pattern "1"
|
11
9
|
end
|
12
10
|
assert_instance_of(Scrubyt::Pattern, pattern)
|
13
11
|
|
@@ -23,7 +21,7 @@ class ExtractorTest < Test::Unit::TestCase
|
|
23
21
|
def test_create_child_pattern
|
24
22
|
pattern = Scrubyt::Extractor.define do
|
25
23
|
fetch File.join(File.dirname(__FILE__), "input/test.html")
|
26
|
-
parent { child "
|
24
|
+
parent { child "2" }
|
27
25
|
end
|
28
26
|
|
29
27
|
assert_equal(pattern.name, "root")
|
@@ -39,10 +37,10 @@ class ExtractorTest < Test::Unit::TestCase
|
|
39
37
|
pattern = Scrubyt::Extractor.define do
|
40
38
|
fetch File.join(File.dirname(__FILE__), "input/test.html")
|
41
39
|
parent do
|
42
|
-
child1 '
|
43
|
-
child2 '
|
44
|
-
child3 '
|
45
|
-
child4 '
|
40
|
+
child1 '1'
|
41
|
+
child2 '2'
|
42
|
+
child3 '3'
|
43
|
+
child4 '4'
|
46
44
|
end
|
47
45
|
end
|
48
46
|
|
@@ -61,7 +59,7 @@ class ExtractorTest < Test::Unit::TestCase
|
|
61
59
|
def test_create_hierarchy
|
62
60
|
tree = Scrubyt::Extractor.define do
|
63
61
|
fetch File.join(File.dirname(__FILE__), "input/test.html")
|
64
|
-
a { b { c { d { e "
|
62
|
+
a { b { c { d { e "1" } } } }
|
65
63
|
end
|
66
64
|
|
67
65
|
assert_equal(tree.name,"root")
|
@@ -76,8 +74,8 @@ class ExtractorTest < Test::Unit::TestCase
|
|
76
74
|
tree = Scrubyt::Extractor.define do
|
77
75
|
fetch File.join(File.dirname(__FILE__), "input/test.html")
|
78
76
|
a do
|
79
|
-
b '
|
80
|
-
c '
|
77
|
+
b '1'
|
78
|
+
c '2'
|
81
79
|
end
|
82
80
|
end
|
83
81
|
|
@@ -86,8 +84,8 @@ class ExtractorTest < Test::Unit::TestCase
|
|
86
84
|
assert_not_nil(tree.children[0].filters[0])
|
87
85
|
assert_nil(tree.children[0].example)
|
88
86
|
assert_not_nil(tree.children[0].children[0].filters[0])
|
89
|
-
assert_equal(tree.children[0].children[0].example,'
|
87
|
+
assert_equal(tree.children[0].children[0].filters[0].example,'1')
|
90
88
|
assert_not_nil(tree.children[0].children[1].filters[0])
|
91
|
-
assert_equal(tree.children[0].children[1].example,'
|
89
|
+
assert_equal(tree.children[0].children[1].filters[0].example,'2')
|
92
90
|
end
|
93
91
|
end
|