sgf_parser 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.irbrc +3 -0
- data/.rvmrc +4 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +17 -0
- data/LICENSE +20 -0
- data/README.textile +27 -0
- data/Rakefile +20 -0
- data/TODO +0 -0
- data/VERSION +1 -0
- data/bin/sgf +28 -0
- data/bin/stm2dot +18 -0
- data/doc/sgf_state_machine.dot +58 -0
- data/doc/sgf_state_machine.svg +269 -0
- data/lib/sgf.rb +15 -0
- data/lib/sgf/binary_file_error.rb +4 -0
- data/lib/sgf/debugger.rb +15 -0
- data/lib/sgf/default_event_listener.rb +37 -0
- data/lib/sgf/model/constants.rb +19 -0
- data/lib/sgf/model/event_listener.rb +72 -0
- data/lib/sgf/model/game.rb +52 -0
- data/lib/sgf/model/label.rb +12 -0
- data/lib/sgf/model/node.rb +115 -0
- data/lib/sgf/model/property_handler.rb +51 -0
- data/lib/sgf/more/state_machine_presenter.rb +43 -0
- data/lib/sgf/more/stm_dot_converter.rb +139 -0
- data/lib/sgf/parse_error.rb +25 -0
- data/lib/sgf/parser.rb +56 -0
- data/lib/sgf/renderer.rb +25 -0
- data/lib/sgf/sgf_helper.rb +55 -0
- data/lib/sgf/sgf_state_machine.rb +174 -0
- data/lib/sgf/state_machine.rb +76 -0
- data/sgf_parser.gemspec +95 -0
- data/spec/fixtures/2009-11-01-1.sgf +24 -0
- data/spec/fixtures/2009-11-01-2.sgf +23 -0
- data/spec/fixtures/chinese_gb.sgf +9 -0
- data/spec/fixtures/chinese_utf.sgf +9 -0
- data/spec/fixtures/example.sgf +18 -0
- data/spec/fixtures/good.sgf +55 -0
- data/spec/fixtures/good1.sgf +167 -0
- data/spec/fixtures/kgs.sgf +723 -0
- data/spec/fixtures/test.png +0 -0
- data/spec/sgf/model/event_listener_spec.rb +97 -0
- data/spec/sgf/model/game_spec.rb +29 -0
- data/spec/sgf/model/node_spec.rb +84 -0
- data/spec/sgf/more/state_machine_presenter_spec.rb +29 -0
- data/spec/sgf/parse_error_spec.rb +10 -0
- data/spec/sgf/parser_spec.rb +210 -0
- data/spec/sgf/sgf_helper_spec.rb +68 -0
- data/spec/sgf/sgf_state_machine_spec.rb +166 -0
- data/spec/sgf/state_machine_spec.rb +137 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +47 -0
- metadata +150 -0
Binary file
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
module SGF
|
4
|
+
module Model
|
5
|
+
describe EventListener do
|
6
|
+
before :each do
|
7
|
+
@listener = EventListener.new
|
8
|
+
end
|
9
|
+
|
10
|
+
(DefaultEventListener.instance_methods - DefaultEventListener.superclass.instance_methods).each do |method|
|
11
|
+
it "should implement #{method} from SGF::DefaultEventListener" do
|
12
|
+
@listener.methods.should include(method)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should start a new game on start_game" do
|
17
|
+
@listener.start_game
|
18
|
+
game1 = @listener.game
|
19
|
+
@listener.start_game
|
20
|
+
game2 = @listener.game
|
21
|
+
game1.should_not == game2
|
22
|
+
end
|
23
|
+
|
24
|
+
it "start_variation should create a new node as variation root" do
|
25
|
+
@listener.start_game
|
26
|
+
@listener.start_node
|
27
|
+
node = @listener.node
|
28
|
+
@listener.start_variation
|
29
|
+
@listener.node.should_not == node
|
30
|
+
@listener.node.variation_root?.should be_true
|
31
|
+
end
|
32
|
+
|
33
|
+
it "end_variation should set current node as parent of current variation's root" do
|
34
|
+
@listener.start_game
|
35
|
+
@listener.start_node
|
36
|
+
node = @listener.node
|
37
|
+
@listener.start_variation
|
38
|
+
@listener.node.should_not == node
|
39
|
+
@listener.end_variation
|
40
|
+
@listener.node.should == node
|
41
|
+
end
|
42
|
+
|
43
|
+
SGF::Model::GAME_PROPERTY_MAPPINGS.each do |prop_name, attr_name|
|
44
|
+
it "call property_name=#{prop_name} and property_value=something should set #{attr_name} on game" do
|
45
|
+
mock(@listener.game).send(attr_name, '123')
|
46
|
+
@listener.property_name = prop_name
|
47
|
+
@listener.property_value = '123'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should set node as game's root node on first call to start_node" do
|
52
|
+
@listener.start_game
|
53
|
+
@listener.start_node
|
54
|
+
@listener.node.should == @listener.game.root_node
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should create child node on current node when call start_node" do
|
58
|
+
@listener.start_game
|
59
|
+
@listener.start_node
|
60
|
+
node = @listener.node
|
61
|
+
@listener.start_node
|
62
|
+
@listener.node.should_not == node
|
63
|
+
@listener.node.parent.should == node
|
64
|
+
node.children.should == [@listener.node]
|
65
|
+
end
|
66
|
+
|
67
|
+
it "property_name='C' and property_value=something should add comment to node" do
|
68
|
+
@listener.start_game
|
69
|
+
@listener.start_node
|
70
|
+
@listener.property_name = "C"
|
71
|
+
@listener.property_value = "comment"
|
72
|
+
@listener.game.root_node.comment.should == 'comment'
|
73
|
+
end
|
74
|
+
|
75
|
+
it "game misc properties" do
|
76
|
+
@listener.start_game
|
77
|
+
@listener.start_node
|
78
|
+
@listener.property_name = "US"
|
79
|
+
@listener.property_value = "A"
|
80
|
+
@listener.game.misc_properties["US"].should == "A"
|
81
|
+
@listener.property_value = "B"
|
82
|
+
@listener.game.misc_properties["US"].should == ["A", "B"]
|
83
|
+
end
|
84
|
+
|
85
|
+
it "node misc properties" do
|
86
|
+
@listener.start_game
|
87
|
+
@listener.start_node
|
88
|
+
@listener.property_name = "TR"
|
89
|
+
@listener.property_value = "AB"
|
90
|
+
@listener.node.misc_properties["TR"].should == "AB"
|
91
|
+
@listener.property_value = "AC"
|
92
|
+
@listener.node.misc_properties["TR"].should == ["AB", "AC"]
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
module SGF
|
4
|
+
module Model
|
5
|
+
describe Game do
|
6
|
+
it "moves should return number of moves" do
|
7
|
+
game = Game.new
|
8
|
+
n1 = Node.new game.root_node
|
9
|
+
n1.sgf_play_black 'AB'
|
10
|
+
n2 = Node.new n1
|
11
|
+
n2.sgf_play_white 'BC'
|
12
|
+
|
13
|
+
game.moves.should == 2
|
14
|
+
end
|
15
|
+
|
16
|
+
it "moves should not count non-move nodes" do
|
17
|
+
game = Game.new
|
18
|
+
n1 = Node.new game.root_node
|
19
|
+
n1.sgf_play_black 'AB'
|
20
|
+
dummy1 = Node.new(n1)
|
21
|
+
n2 = Node.new dummy1
|
22
|
+
n2.sgf_play_white 'BC'
|
23
|
+
dummy2 = Node.new(n2)
|
24
|
+
|
25
|
+
game.moves.should == 2
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
module SGF
|
4
|
+
module Model
|
5
|
+
describe Node do
|
6
|
+
describe "move_no" do
|
7
|
+
it "should return parent node's move number + 1 if current node is a move" do
|
8
|
+
parent = Node.new
|
9
|
+
mock(parent).move_no{ 10 }
|
10
|
+
node = Node.new(parent)
|
11
|
+
node.sgf_play_black 'AB'
|
12
|
+
node.move_no.should == 11
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should return parent node's move number if current node is not a move" do
|
16
|
+
parent = Node.new
|
17
|
+
mock(parent).move_no{ 10 }
|
18
|
+
node = Node.new(parent)
|
19
|
+
node.move_no.should == 10
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should return 0 if parent is nil and current node is not a move" do
|
23
|
+
node = Node.new
|
24
|
+
node.move_no.should == 0
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should return 1 if parent is nil and current node is a move" do
|
28
|
+
node = Node.new
|
29
|
+
node.sgf_play_black "AB"
|
30
|
+
node.move_no.should == 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "sgf_setup_black should add an entry to black_moves" do
|
35
|
+
node = Node.new
|
36
|
+
node.sgf_setup_black "AB"
|
37
|
+
node.black_moves.first.should == [0, 1]
|
38
|
+
end
|
39
|
+
|
40
|
+
it "sgf_setup_white should add an entry to white_moves" do
|
41
|
+
node = Node.new
|
42
|
+
node.sgf_setup_white "AB"
|
43
|
+
node.white_moves.first.should == [0, 1]
|
44
|
+
end
|
45
|
+
|
46
|
+
it "sgf_play_black should set node type to MOVE and save move information" do
|
47
|
+
node = Node.new
|
48
|
+
node.sgf_play_black "AB"
|
49
|
+
node.node_type.should == Constants::NODE_MOVE
|
50
|
+
node.color.should == Constants::BLACK
|
51
|
+
node.move.should == [0, 1]
|
52
|
+
end
|
53
|
+
|
54
|
+
it "sgf_play_black should set node type to PASS on empty move coordinate" do
|
55
|
+
node = Node.new
|
56
|
+
node.sgf_play_black " "
|
57
|
+
node.node_type.should == Constants::NODE_PASS
|
58
|
+
node.color.should == Constants::BLACK
|
59
|
+
end
|
60
|
+
|
61
|
+
it "sgf_play_white should set node type to MOVE and save move information" do
|
62
|
+
node = Node.new
|
63
|
+
node.sgf_play_white "AB"
|
64
|
+
node.node_type.should == Constants::NODE_MOVE
|
65
|
+
node.color.should == Constants::WHITE
|
66
|
+
node.move.should == [0, 1]
|
67
|
+
end
|
68
|
+
|
69
|
+
it "sgf_play_white should set node type to PASS on empty move coordinate" do
|
70
|
+
node = Node.new
|
71
|
+
node.sgf_play_white " "
|
72
|
+
node.node_type.should == Constants::NODE_PASS
|
73
|
+
node.color.should == Constants::WHITE
|
74
|
+
end
|
75
|
+
|
76
|
+
it "child returns first child" do
|
77
|
+
node = Node.new
|
78
|
+
first_child = Node.new(node)
|
79
|
+
second_child = Node.new(node)
|
80
|
+
node.child.should == first_child
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../../lib/sgf/more/state_machine_presenter')
|
3
|
+
|
4
|
+
describe SGF::More::StateMachinePresenter do
|
5
|
+
it "should create nodes for start state and all reachable states" do
|
6
|
+
stm = SGF::StateMachine.new(:start)
|
7
|
+
stm.transition(:start, /a/, :state_a)
|
8
|
+
|
9
|
+
presenter = SGF::More::StateMachinePresenter.new
|
10
|
+
presenter.process(stm)
|
11
|
+
|
12
|
+
presenter.nodes.should include("start")
|
13
|
+
presenter.nodes.should include("state_a")
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should create edges for all transitions" do
|
17
|
+
stm = SGF::StateMachine.new(:start)
|
18
|
+
stm.desc (desc1 = "start + /a/ => state_a")
|
19
|
+
stm.transition(:start, /a/, :state_a)
|
20
|
+
stm.desc (desc2 = "start + /b/ => state_b")
|
21
|
+
stm.transition(:start, /b/, :state_b)
|
22
|
+
|
23
|
+
presenter = SGF::More::StateMachinePresenter.new
|
24
|
+
presenter.process(stm)
|
25
|
+
|
26
|
+
presenter.edges.should include(["start", "state_a", desc1])
|
27
|
+
presenter.edges.should include(["start", "state_b", desc2])
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
module SGF
|
4
|
+
describe ParseError do
|
5
|
+
it "message should return content up to the bad input" do
|
6
|
+
error = ParseError.new("(;AB]", 4)
|
7
|
+
error.message.should include("(;AB] <=== SGF parse error occurred here")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
module SGF
|
4
|
+
describe Parser, 'with DefaultEventListener' do
|
5
|
+
before :each do
|
6
|
+
@listener = DefaultEventListener.new
|
7
|
+
@parser = Parser.new @listener
|
8
|
+
end
|
9
|
+
|
10
|
+
{'nil' => nil, '' => '', ' ' => ' '}.each do |name, value|
|
11
|
+
it "should throw error on bad input - '#{name}'" do
|
12
|
+
begin
|
13
|
+
@parser.parse(value)
|
14
|
+
fail("Should raise error")
|
15
|
+
rescue => e
|
16
|
+
e.class.should == ArgumentError
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should call start_game" do
|
22
|
+
mock(@listener).start_game
|
23
|
+
@parser.parse("(")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should call end_game" do
|
27
|
+
mock(@listener).end_game
|
28
|
+
@parser.parse("(;)")
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should call start_node" do
|
32
|
+
mock(@listener).start_node
|
33
|
+
@parser.parse("(;")
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should call end_variation" do
|
37
|
+
mock(@listener).end_variation
|
38
|
+
@parser.parse("(;)")
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should call property_name= with name" do
|
42
|
+
mock(@listener).property_name = 'GN'
|
43
|
+
@parser.parse("(;GN[)")
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should call property_value= with value" do
|
47
|
+
mock(@listener).property_value = 'a game'
|
48
|
+
@parser.parse("(;GN[a game])")
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should call property_value= for each value" do
|
52
|
+
mock(@listener).property_value = 'DB'
|
53
|
+
mock(@listener).property_value = 'KS'
|
54
|
+
@parser.parse("(;AB[DB][KS])")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should call start_variation" do
|
58
|
+
mock(@listener).start_variation
|
59
|
+
@parser.parse("(;(")
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should call end_variation" do
|
63
|
+
mock(@listener).end_variation
|
64
|
+
@parser.parse("(;(;)")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe Parser, 'with SGF::Model::EventListener' do
|
69
|
+
before :each do
|
70
|
+
@listener = SGF::Model::EventListener.new
|
71
|
+
@parser = Parser.new @listener
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should parse game in file" do
|
75
|
+
@parser.parse_file File.expand_path(File.dirname(__FILE__) + '/../fixtures/good.sgf')
|
76
|
+
game = @listener.game
|
77
|
+
game.name.should == 'White (W) vs. Black (B)'
|
78
|
+
game.rule.should == 'Japanese'
|
79
|
+
game.board_size.should == 19
|
80
|
+
game.handicap.should == 0
|
81
|
+
game.komi.should == 5.5
|
82
|
+
game.played_on.should == "1999-07-28"
|
83
|
+
game.white_player.should == 'White'
|
84
|
+
game.black_player.should == 'Black'
|
85
|
+
game.program.should == 'Cgoban 1.9.2'
|
86
|
+
game.time_rule.should == '30:00(5x1:00)'
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should raise BinaryFileError on parsing binary file" do
|
90
|
+
pending 'check if file is binary is problematic'
|
91
|
+
lambda {
|
92
|
+
@parser.parse_file File.expand_path(File.dirname(__FILE__) + '/../fixtures/test.png')
|
93
|
+
}.should raise_error(SGF::BinaryFileError)
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should parse game without moves" do
|
97
|
+
@parser.parse <<-INPUT
|
98
|
+
(;GM[1]FF[3]
|
99
|
+
GN[White (W) vs. Black (B)];
|
100
|
+
)
|
101
|
+
INPUT
|
102
|
+
game = @listener.game
|
103
|
+
game.name.should == 'White (W) vs. Black (B)'
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should raise error on invalid input" do
|
107
|
+
lambda { @parser.parse("123") }.should raise_error
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should parse a complete game" do
|
111
|
+
@parser.parse <<-INPUT
|
112
|
+
(;GM[1]FF[3]
|
113
|
+
RU[Japanese]SZ[19]HA[0]KM[5.5]
|
114
|
+
PW[White]
|
115
|
+
PB[Black]
|
116
|
+
GN[White (W) vs. Black (B)]
|
117
|
+
DT[1999-07-28]
|
118
|
+
RE[W+R]
|
119
|
+
SY[Cgoban 1.9.2]TM[30:00(5x1:00)];
|
120
|
+
AW[ea][eb][ec][bd][dd][ae][ce][de][cf][ef][cg][dg][eh][ci][di][bj][ej]
|
121
|
+
AB[da][db][cc][dc][cd][be][bf][ag][bg][bh][ch][dh]LB[bd:A]PL[2]
|
122
|
+
C[guff plays A and adum tenukis to fill a 1-point ko. white to kill.
|
123
|
+
]
|
124
|
+
(;W[bc];B[bb]
|
125
|
+
(;W[ca];B[cb]
|
126
|
+
(;W[ab];B[ba]
|
127
|
+
(;W[bi]
|
128
|
+
C[RIGHT black can't push (but no such luck in the actual game)
|
129
|
+
])
|
130
|
+
)))
|
131
|
+
)
|
132
|
+
INPUT
|
133
|
+
game = @listener.game
|
134
|
+
game.name.should == 'White (W) vs. Black (B)'
|
135
|
+
game.rule.should == 'Japanese'
|
136
|
+
game.board_size.should == 19
|
137
|
+
game.handicap.should == 0
|
138
|
+
game.komi.should == 5.5
|
139
|
+
game.result.should == 'W+R'
|
140
|
+
game.played_on.should == "1999-07-28"
|
141
|
+
game.white_player.should == 'White'
|
142
|
+
game.black_player.should == 'Black'
|
143
|
+
game.program.should == 'Cgoban 1.9.2'
|
144
|
+
game.time_rule.should == '30:00(5x1:00)'
|
145
|
+
|
146
|
+
root_node = game.root_node
|
147
|
+
root_node.node_type.should == SGF::Model::Constants::NODE_SETUP
|
148
|
+
|
149
|
+
node2 = root_node.child
|
150
|
+
node2.node_type.should == SGF::Model::Constants::NODE_SETUP
|
151
|
+
node2.trunk?.should be_true
|
152
|
+
node2.comment.should == "guff plays A and adum tenukis to fill a 1-point ko. white to kill."
|
153
|
+
node2.black_moves.should include([3, 0])
|
154
|
+
node2.black_moves.should include([3, 1])
|
155
|
+
node2.white_moves.should include([4, 0])
|
156
|
+
node2.white_moves.should include([4, 1])
|
157
|
+
node2.labels[0].should == SGF::Model::Label.new([1, 3], "A")
|
158
|
+
|
159
|
+
var1_root = node2.children[0]
|
160
|
+
var1_root.trunk?.should be_false
|
161
|
+
var1_root.variation_root?.should be_true
|
162
|
+
var1_root.color.should == SGF::Model::Constants::WHITE
|
163
|
+
var1_root.move.should == [1, 2]
|
164
|
+
|
165
|
+
var1_node2 = var1_root.child
|
166
|
+
var1_node2.trunk?.should be_false
|
167
|
+
var1_node2.variation_root?.should_not be_true
|
168
|
+
var1_node2.color.should == SGF::Model::Constants::BLACK
|
169
|
+
var1_node2.move.should == [1, 1]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe "class methods" do
|
174
|
+
it "parse should take a String and parse it and return the game" do
|
175
|
+
game = SGF::Parser.parse <<-INPUT
|
176
|
+
(;GM[1]FF[3]
|
177
|
+
GN[White (W) vs. Black (B)];
|
178
|
+
)
|
179
|
+
INPUT
|
180
|
+
game.name.should == 'White (W) vs. Black (B)'
|
181
|
+
end
|
182
|
+
|
183
|
+
it "parse should pass debug parameter to event listener" do
|
184
|
+
event_listener = SGF::Model::EventListener.new(false)
|
185
|
+
mock(SGF::Model::EventListener).new(true) {event_listener}
|
186
|
+
SGF::Parser.parse "(;)", true
|
187
|
+
end
|
188
|
+
|
189
|
+
it "parse_file should take a sgf file and parse it and return the game" do
|
190
|
+
game = SGF::Parser.parse_file File.expand_path(File.dirname(__FILE__) + '/../fixtures/good.sgf')
|
191
|
+
game.name.should == 'White (W) vs. Black (B)'
|
192
|
+
end
|
193
|
+
|
194
|
+
it "parse_file should pass debug parameter to event listener" do
|
195
|
+
event_listener = SGF::Model::EventListener.new(false)
|
196
|
+
mock(SGF::Model::EventListener).new(true) {event_listener}
|
197
|
+
game = SGF::Parser.parse_file(File.expand_path(File.dirname(__FILE__) + '/../fixtures/good.sgf'), true)
|
198
|
+
game.name.should == 'White (W) vs. Black (B)'
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should parse game with escaped []" do
|
202
|
+
game = SGF::Parser.parse_file(File.expand_path(File.dirname(__FILE__) + '/../fixtures/good1.sgf'))
|
203
|
+
end
|
204
|
+
|
205
|
+
it "is_binary? should return true for binary file" do
|
206
|
+
pending 'check if file is binary is problematic'
|
207
|
+
SGF::Parser.is_binary?(File.expand_path(File.dirname(__FILE__) + '/../fixtures/test.png')).should be_true
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|