atp 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/config/commands.rb +5 -7
- data/config/version.rb +2 -2
- data/lib/atp.rb +45 -14
- data/lib/atp/ast/node.rb +18 -9
- data/lib/atp/flow.rb +558 -151
- data/lib/atp/flow_api.rb +26 -0
- data/lib/atp/formatters/basic.rb +3 -1
- data/lib/atp/processor.rb +0 -16
- data/lib/atp/processors/add_ids.rb +1 -0
- data/lib/atp/processors/adjacent_if_combiner.rb +97 -0
- data/lib/atp/processors/append_to.rb +27 -0
- data/lib/atp/processors/apply_post_group_actions.rb +50 -0
- data/lib/atp/processors/condition.rb +38 -37
- data/lib/atp/processors/continue_implementer.rb +35 -0
- data/lib/atp/processors/else_remover.rb +31 -0
- data/lib/atp/processors/empty_branch_remover.rb +17 -0
- data/lib/atp/processors/extract_set_flags.rb +18 -0
- data/lib/atp/processors/flag_optimizer.rb +214 -0
- data/lib/atp/processors/flattener.rb +58 -0
- data/lib/atp/processors/flow_id.rb +10 -4
- data/lib/atp/processors/on_pass_fail_remover.rb +39 -0
- data/lib/atp/processors/one_flag_per_test.rb +79 -0
- data/lib/atp/processors/pre_cleaner.rb +13 -8
- data/lib/atp/processors/redundant_condition_remover.rb +28 -0
- data/lib/atp/processors/relationship.rb +91 -53
- data/lib/atp/runner.rb +41 -31
- data/lib/atp/validator.rb +19 -0
- data/lib/atp/validators/duplicate_ids.rb +2 -2
- data/lib/atp/validators/jobs.rb +12 -10
- data/lib/atp/validators/missing_ids.rb +14 -8
- metadata +15 -5
- data/lib/atp/ast/builder.rb +0 -397
- data/lib/atp/ast/factories.rb +0 -17
- data/lib/atp/processors/post_cleaner.rb +0 -43
@@ -0,0 +1,18 @@
|
|
1
|
+
module ATP
|
2
|
+
module Processors
|
3
|
+
# Extracts all flags which are set within the given flow, returning
|
4
|
+
# them in an array
|
5
|
+
class ExtractSetFlags < ATP::Processor
|
6
|
+
def run(nodes)
|
7
|
+
@results = []
|
8
|
+
process_all(nodes)
|
9
|
+
@results.uniq
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_set_flag(node)
|
13
|
+
flag = node.value
|
14
|
+
@results << flag
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
module ATP
|
2
|
+
module Processors
|
3
|
+
# This processor eliminates the use of run flags between adjacent tests:
|
4
|
+
#
|
5
|
+
# s(:flow,
|
6
|
+
# s(:name, "prb1"),
|
7
|
+
# s(:test,
|
8
|
+
# s(:name, "test1"),
|
9
|
+
# s(:id, "t1"),
|
10
|
+
# s(:on_fail,
|
11
|
+
# s(:set_flag, "t1_FAILED", "auto_generated"),
|
12
|
+
# s(:continue))),
|
13
|
+
# s(:if_flag, "t1_FAILED",
|
14
|
+
# s(:test,
|
15
|
+
# s(:name, "test2"))))
|
16
|
+
#
|
17
|
+
#
|
18
|
+
# s(:flow,
|
19
|
+
# s(:name, "prb1"),
|
20
|
+
# s(:test,
|
21
|
+
# s(:name, "test1"),
|
22
|
+
# s(:id, "t1"),
|
23
|
+
# s(:on_fail,
|
24
|
+
# s(:test,
|
25
|
+
# s(:name, "test2")))))
|
26
|
+
#
|
27
|
+
class FlagOptimizer < Processor
|
28
|
+
attr_reader :run_flag_table
|
29
|
+
|
30
|
+
class ExtractRunFlagTable < Processor
|
31
|
+
# Hash table of run_flag name with number of times used
|
32
|
+
attr_reader :run_flag_table
|
33
|
+
|
34
|
+
# Reset hash table
|
35
|
+
def initialize
|
36
|
+
@run_flag_table = {}.with_indifferent_access
|
37
|
+
end
|
38
|
+
|
39
|
+
# For run_flag nodes, increment # of occurrences for specified flag
|
40
|
+
def on_if_flag(node)
|
41
|
+
children = node.children.dup
|
42
|
+
names = children.shift
|
43
|
+
state = node.type == :if_flag
|
44
|
+
Array(names).each do |name|
|
45
|
+
if @run_flag_table[name.to_sym].nil?
|
46
|
+
@run_flag_table[name.to_sym] = 1
|
47
|
+
else
|
48
|
+
@run_flag_table[name.to_sym] += 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
process_all(node.children)
|
52
|
+
end
|
53
|
+
alias_method :on_unless_flag, :on_if_flag
|
54
|
+
end
|
55
|
+
|
56
|
+
def run(node)
|
57
|
+
# Pre-process the AST for # of occurrences of each run-flag used
|
58
|
+
t = ExtractRunFlagTable.new
|
59
|
+
t.process(node)
|
60
|
+
@run_flag_table = t.run_flag_table
|
61
|
+
extract_volatiles(node)
|
62
|
+
process(node)
|
63
|
+
end
|
64
|
+
|
65
|
+
def on_named_collection(node)
|
66
|
+
name, *nodes = *node
|
67
|
+
node.updated(nil, [name] + optimize(process_all(nodes)))
|
68
|
+
end
|
69
|
+
alias_method :on_flow, :on_named_collection
|
70
|
+
alias_method :on_group, :on_named_collection
|
71
|
+
alias_method :on_unless_flag, :on_named_collection
|
72
|
+
|
73
|
+
def on_if_flag(node)
|
74
|
+
name, *nodes = *node
|
75
|
+
# Remove this node and return its children if required
|
76
|
+
if if_run_flag_to_remove.last == node.to_a[0]
|
77
|
+
node.updated(:inline, node.to_a[1..-1])
|
78
|
+
else
|
79
|
+
node.updated(nil, [name] + optimize(process_all(nodes)))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def on_on_fail(node)
|
84
|
+
if to_inline = nodes_to_inline_on_pass_or_fail.last
|
85
|
+
# If this node sets the flag that gates the node to be inlined
|
86
|
+
set_flag = node.find(:set_flag)
|
87
|
+
if set_flag && gated_by_set?(set_flag.to_a[0], to_inline)
|
88
|
+
# Remove the sub-node that sets the flag if there are no further references to it
|
89
|
+
|
90
|
+
if @run_flag_table[set_flag.to_a[0]] == 1 || !@run_flag_table[set_flag.to_a[0]]
|
91
|
+
node = node.updated(nil, node.children - [set_flag])
|
92
|
+
end
|
93
|
+
|
94
|
+
# And append the content of the node to be in_lined at the end of this on pass/fail node
|
95
|
+
append = reorder_nested_run_flags(set_flag.to_a[0], to_inline).to_a[1..-1]
|
96
|
+
|
97
|
+
# Belt and braces approach to make sure this node to be inlined does
|
98
|
+
# not get picked up anywhere else
|
99
|
+
nodes_to_inline_on_pass_or_fail.pop
|
100
|
+
nodes_to_inline_on_pass_or_fail << nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
node.updated(nil, optimize(process_all(node.children + Array(append))))
|
104
|
+
end
|
105
|
+
alias_method :on_on_pass, :on_on_fail
|
106
|
+
|
107
|
+
def optimize(nodes)
|
108
|
+
results = []
|
109
|
+
node1 = nil
|
110
|
+
nodes.each do |node2|
|
111
|
+
if node1
|
112
|
+
if can_be_combined?(node1, node2)
|
113
|
+
node1 = combine(node1, node2)
|
114
|
+
else
|
115
|
+
results << node1
|
116
|
+
node1 = node2
|
117
|
+
end
|
118
|
+
else
|
119
|
+
node1 = node2
|
120
|
+
end
|
121
|
+
end
|
122
|
+
results << node1 if node1
|
123
|
+
results
|
124
|
+
end
|
125
|
+
|
126
|
+
def can_be_combined?(node1, node2)
|
127
|
+
if node1.type == :test && (node2.type == :if_flag || node2.type == :unless_flag)
|
128
|
+
if node1.find_all(:on_fail, :on_pass).any? do |node|
|
129
|
+
if n = node.find(:set_flag)
|
130
|
+
# Inline instead of setting a flag if...
|
131
|
+
gated_by_set?(n.to_a[0], node2) && # The flag set by node1 is gating node2
|
132
|
+
n.to_a[1] == 'auto_generated' && # The flag has been generated and not specified by the user
|
133
|
+
n.to_a[0] !~ /_RAN$/ && # And don't compress RAN flags because they can be set by both on_fail and on_pass
|
134
|
+
!volatile?(n.to_a[0]) # And make sure the flag has not been marked as volatile
|
135
|
+
end
|
136
|
+
end
|
137
|
+
return true
|
138
|
+
end
|
139
|
+
end
|
140
|
+
false
|
141
|
+
end
|
142
|
+
|
143
|
+
def combine(node1, node2)
|
144
|
+
nodes_to_inline_on_pass_or_fail << node2
|
145
|
+
node1 = node1.updated(nil, process_all(node1.children))
|
146
|
+
nodes_to_inline_on_pass_or_fail.pop
|
147
|
+
node1
|
148
|
+
end
|
149
|
+
|
150
|
+
# node will always be an if_flag or unless_flag type node, guaranteed by the caller
|
151
|
+
#
|
152
|
+
# Returns true if flag matches the one supplied
|
153
|
+
#
|
154
|
+
# s(:if_flag, flag,
|
155
|
+
# s(:test, ...
|
156
|
+
#
|
157
|
+
# Also returns true if flag matches the one supplied, but it is nested within other flag conditions:
|
158
|
+
#
|
159
|
+
# s(:unless_flag, other_flag,
|
160
|
+
# s(:if_flag, other_flag2,
|
161
|
+
# s(:if_flag, flag,
|
162
|
+
# s(:test, ...
|
163
|
+
def gated_by_set?(flag, node)
|
164
|
+
(flag == node.to_a[0] && node.type == :if_flag) ||
|
165
|
+
(node.to_a.size == 2 && (node.to_a.last.type == :if_flag || node.to_a.last.type == :unless_flag) && gated_by_set?(flag, node.to_a.last))
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns the node with the run_flag clauses re-ordered to have the given flag of interest at the top.
|
169
|
+
#
|
170
|
+
# The caller guarantees the run_flag clause containing the given flag is present.
|
171
|
+
#
|
172
|
+
# For example, given this node:
|
173
|
+
#
|
174
|
+
# s(:unless_flag, "flag1",
|
175
|
+
# s(:if_flag, "ot_BEA7F3B_FAILED",
|
176
|
+
# s(:test,
|
177
|
+
# s(:object, <TestSuite: inner_test1_BEA7F3B>),
|
178
|
+
# s(:name, "inner_test1_BEA7F3B"),
|
179
|
+
# s(:number, 0),
|
180
|
+
# s(:id, "it1_BEA7F3B"),
|
181
|
+
# s(:on_fail,
|
182
|
+
# s(:render, "multi_bin;")))))
|
183
|
+
#
|
184
|
+
# Then this node would be returned when the flag of interest is ot_BEA7F3B_FAILED:
|
185
|
+
#
|
186
|
+
# s(:if_flag, "ot_BEA7F3B_FAILED",
|
187
|
+
# s(:unless_flag, "flag1",
|
188
|
+
# s(:test,
|
189
|
+
# s(:object, <TestSuite: inner_test1_BEA7F3B>),
|
190
|
+
# s(:name, "inner_test1_BEA7F3B"),
|
191
|
+
# s(:number, 0),
|
192
|
+
# s(:id, "it1_BEA7F3B"),
|
193
|
+
# s(:on_fail,
|
194
|
+
# s(:render, "multi_bin;")))))
|
195
|
+
def reorder_nested_run_flags(flag, node)
|
196
|
+
# If the run_flag we care about is already at the top, just return node
|
197
|
+
unless node.to_a[0] == flag && node.type == :if_flag
|
198
|
+
if_run_flag_to_remove << flag
|
199
|
+
node = node.updated(:if_flag, [flag] + [process(node)])
|
200
|
+
if_run_flag_to_remove.pop
|
201
|
+
end
|
202
|
+
node
|
203
|
+
end
|
204
|
+
|
205
|
+
def if_run_flag_to_remove
|
206
|
+
@if_run_flag_to_remove ||= []
|
207
|
+
end
|
208
|
+
|
209
|
+
def nodes_to_inline_on_pass_or_fail
|
210
|
+
@nodes_to_inline_on_pass_or_fail ||= []
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ATP
|
2
|
+
module Processors
|
3
|
+
# Gives every node their own individual wrapping of condition nodes. No attempt is made
|
4
|
+
# to identify or remove duplicate conditions in the wrapping, that will be done later by
|
5
|
+
# the RedundantConditionRemover.
|
6
|
+
class Flattener < Processor
|
7
|
+
def run(node)
|
8
|
+
@results = [[]]
|
9
|
+
@conditions = []
|
10
|
+
process(node)
|
11
|
+
node.updated(:flow, results)
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_flow(node)
|
15
|
+
process_all(node.children)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Handles the top-level flow nodes
|
19
|
+
def on_volatile(node)
|
20
|
+
results << node
|
21
|
+
end
|
22
|
+
alias_method :on_name, :on_volatile
|
23
|
+
alias_method :on_id, :on_volatile
|
24
|
+
|
25
|
+
def on_group(node)
|
26
|
+
@results << []
|
27
|
+
process_all(node.children)
|
28
|
+
nodes = @results.pop
|
29
|
+
results << node.updated(nil, nodes)
|
30
|
+
end
|
31
|
+
|
32
|
+
def on_condition_node(node)
|
33
|
+
flag, *nodes = *node
|
34
|
+
@conditions << node.updated(node.type, [flag])
|
35
|
+
process_all(nodes)
|
36
|
+
@conditions.pop
|
37
|
+
end
|
38
|
+
ATP::Flow::CONDITION_NODE_TYPES.each do |type|
|
39
|
+
alias_method "on_#{type}", :on_condition_node unless method_defined?("on_#{type}")
|
40
|
+
end
|
41
|
+
|
42
|
+
def handler_missing(node)
|
43
|
+
results << wrap_with_current_conditions(node)
|
44
|
+
end
|
45
|
+
|
46
|
+
def wrap_with_current_conditions(node)
|
47
|
+
@conditions.reverse_each do |condition|
|
48
|
+
node = condition.updated(nil, condition.children + [node])
|
49
|
+
end
|
50
|
+
node
|
51
|
+
end
|
52
|
+
|
53
|
+
def results
|
54
|
+
@results.last
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -17,8 +17,8 @@ module ATP
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
21
|
-
tid,
|
20
|
+
def on_if_failed(node)
|
21
|
+
tid, *nodes = *node
|
22
22
|
if tid.is_a?(Array)
|
23
23
|
tid = tid.map do |tid|
|
24
24
|
if tid =~ /^extern/
|
@@ -32,9 +32,15 @@ module ATP
|
|
32
32
|
tid = "#{tid}_#{id}"
|
33
33
|
end
|
34
34
|
end
|
35
|
-
node.updated(nil, [tid
|
35
|
+
node.updated(nil, [tid] + process_all(nodes))
|
36
36
|
end
|
37
|
-
alias_method :
|
37
|
+
alias_method :on_if_any_failed, :on_if_failed
|
38
|
+
alias_method :on_if_all_failed, :on_if_failed
|
39
|
+
alias_method :on_if_passed, :on_if_failed
|
40
|
+
alias_method :on_if_any_passed, :on_if_failed
|
41
|
+
alias_method :on_if_all_passed, :on_if_failed
|
42
|
+
alias_method :on_if_ran, :on_if_failed
|
43
|
+
alias_method :on_unless_ran, :on_if_failed
|
38
44
|
end
|
39
45
|
end
|
40
46
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ATP
|
2
|
+
module Processors
|
3
|
+
# Removes most things from embedded on_pass/fail nodes and converts them to the equivalent
|
4
|
+
# on_passed/failed condition at the same level as the parent node
|
5
|
+
class OnPassFailRemover < Processor
|
6
|
+
def run(node)
|
7
|
+
process(node)
|
8
|
+
end
|
9
|
+
|
10
|
+
def on_test(node)
|
11
|
+
on_pass = node.find(:on_pass)
|
12
|
+
on_fail = node.find(:on_fail)
|
13
|
+
if on_pass || on_fail
|
14
|
+
id = node.find(:id)
|
15
|
+
unless id
|
16
|
+
fail 'Something has gone wrong, all nodes should have IDs by this point'
|
17
|
+
end
|
18
|
+
id = id.value
|
19
|
+
nodes = [node]
|
20
|
+
if on_fail && contains_anything_interesting?(on_fail)
|
21
|
+
nodes << node.updated(:if_failed, [id] + on_fail.children)
|
22
|
+
nodes[0] = nodes[0].remove(on_fail)
|
23
|
+
end
|
24
|
+
if on_pass && contains_anything_interesting?(on_pass)
|
25
|
+
nodes << node.updated(:if_passed, [id] + on_pass.children)
|
26
|
+
nodes[0] = nodes[0].remove(on_pass)
|
27
|
+
end
|
28
|
+
node.updated(:inline, nodes)
|
29
|
+
else
|
30
|
+
node.updated(nil, process_all(node.children))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def contains_anything_interesting?(node)
|
35
|
+
node.children.any? { |n| n.type != :set_result && n.type != :continue && n.type != :set_flag }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module ATP
|
2
|
+
module Processors
|
3
|
+
# Ensures that all test nodes only ever set a flag once
|
4
|
+
class OneFlagPerTest < Processor
|
5
|
+
def run(node)
|
6
|
+
@build_table = true
|
7
|
+
@pass_table = {}
|
8
|
+
@fail_table = {}
|
9
|
+
process(node)
|
10
|
+
@counters = {}
|
11
|
+
@pass_table.each { |f, v| @counters[f] = 0 }
|
12
|
+
@fail_table.each { |f, v| @counters[f] = 0 }
|
13
|
+
@build_table = false
|
14
|
+
process(node)
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_test(node)
|
18
|
+
on_pass = node.find(:on_pass)
|
19
|
+
on_fail = node.find(:on_fail)
|
20
|
+
if @build_table
|
21
|
+
if on_fail
|
22
|
+
on_fail.find_all(:set_flag).each do |n|
|
23
|
+
@fail_table[n.to_a[0]] ||= 0
|
24
|
+
@fail_table[n.to_a[0]] += 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
if on_pass
|
28
|
+
on_pass.find_all(:set_flag).each do |n|
|
29
|
+
@pass_table[n.to_a[0]] ||= 0
|
30
|
+
@pass_table[n.to_a[0]] += 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
else
|
34
|
+
to_be_set = {}
|
35
|
+
if on_fail
|
36
|
+
node = node.remove(on_fail)
|
37
|
+
on_fail.find_all(:set_flag).each do |set_flag|
|
38
|
+
old_flag = set_flag.to_a[0]
|
39
|
+
if @fail_table[old_flag] > 1
|
40
|
+
on_fail = on_fail.remove(set_flag)
|
41
|
+
new_flag = "#{old_flag}_#{@counters[old_flag]}"
|
42
|
+
@counters[old_flag] += 1
|
43
|
+
to_be_set[old_flag] = new_flag
|
44
|
+
c = set_flag.children.dup
|
45
|
+
c[0] = new_flag
|
46
|
+
set_flag = set_flag.updated(nil, c)
|
47
|
+
on_fail = on_fail.updated(nil, on_fail.children + [set_flag])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
node = node.updated(nil, node.children + [on_fail])
|
51
|
+
end
|
52
|
+
if on_pass
|
53
|
+
node = node.remove(on_pass)
|
54
|
+
on_pass.find_all(:set_flag).each do |set_flag|
|
55
|
+
old_flag = set_flag.to_a[0]
|
56
|
+
if @pass_table[old_flag] > 1
|
57
|
+
on_pass = on_pass.remove(set_flag)
|
58
|
+
new_flag = "#{old_flag}_#{@counters[old_flag]}"
|
59
|
+
@counters[old_flag] += 1
|
60
|
+
to_be_set[old_flag] = new_flag
|
61
|
+
c = set_flag.children.dup
|
62
|
+
c[0] = new_flag
|
63
|
+
set_flag = set_flag.updated(nil, c)
|
64
|
+
on_pass = on_pass.updated(nil, on_pass.children + [set_flag])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
node = node.updated(nil, node.children + [on_pass])
|
68
|
+
end
|
69
|
+
if to_be_set.empty?
|
70
|
+
node
|
71
|
+
else
|
72
|
+
nodes = to_be_set.map { |old, new| node.updated(:if_flag, [new, node.updated(:set_flag, [old, 'auto_generated'])]) }
|
73
|
+
node.updated(:inline, [node] + nodes)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
module ATP
|
2
2
|
module Processors
|
3
3
|
# Modifies the AST by performing some basic clean up, mainly to sanitize
|
4
|
-
# user input. For example it will ensure that all IDs
|
5
|
-
#
|
4
|
+
# user input. For example it will ensure that all IDs and references are underscored
|
5
|
+
# and lower cased.
|
6
6
|
class PreCleaner < Processor
|
7
7
|
def initialize
|
8
8
|
@group_ids = []
|
@@ -15,12 +15,17 @@ module ATP
|
|
15
15
|
end
|
16
16
|
|
17
17
|
# Make all ID references use the lower case symbols
|
18
|
-
def
|
19
|
-
children = node
|
20
|
-
|
21
|
-
node.updated(nil, process_all(children))
|
18
|
+
def on_if_failed(node)
|
19
|
+
id, *children = *node
|
20
|
+
node.updated(nil, [clean(id)] + process_all(children))
|
22
21
|
end
|
23
|
-
alias_method :
|
22
|
+
alias_method :on_if_passed, :on_if_failed
|
23
|
+
alias_method :on_if_any_failed, :on_if_failed
|
24
|
+
alias_method :on_if_all_failed, :on_if_failed
|
25
|
+
alias_method :on_if_any_passed, :on_if_failed
|
26
|
+
alias_method :on_if_all_passed, :on_if_failed
|
27
|
+
alias_method :on_if_ran, :on_if_failed
|
28
|
+
alias_method :on_unless_ran, :on_if_failed
|
24
29
|
|
25
30
|
def on_group(node)
|
26
31
|
if id = node.children.find { |n| n.type == :id }
|
@@ -51,7 +56,7 @@ module ATP
|
|
51
56
|
if id.is_a?(Array)
|
52
57
|
id.map { |i| clean(i) }
|
53
58
|
else
|
54
|
-
id.to_s.
|
59
|
+
id.to_s.symbolize.to_s
|
55
60
|
end
|
56
61
|
end
|
57
62
|
end
|