ios_parser 0.5.1-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,337 @@
1
+ require_relative '../../spec_helper'
2
+ require 'ios_parser'
3
+ require 'ios_parser/lexer'
4
+
5
+ module IOSParser
6
+ describe IOS do
7
+ context 'indented region' do
8
+ let(:input) { <<-END.unindent }
9
+ policy-map mypolicy_in
10
+ class myservice_service
11
+ police 300000000 1000000 exceed-action policed-dscp-transmit
12
+ set dscp cs1
13
+ class other_service
14
+ police 600000000 1000000 exceed-action policed-dscp-transmit
15
+ set dscp cs2
16
+ command_with_no_args
17
+ END
18
+
19
+ let(:output) do
20
+ {
21
+ commands:
22
+ [{ args: ['policy-map', 'mypolicy_in'],
23
+ commands:
24
+ [{ args: %w[class myservice_service],
25
+ commands: [{ args: ['police', 300_000_000, 1_000_000,
26
+ 'exceed-action',
27
+ 'policed-dscp-transmit'],
28
+ commands: [{ args: %w[set dscp cs1],
29
+ commands: [], pos: 114 }],
30
+ pos: 50 }],
31
+ pos: 24 },
32
+
33
+ { args: %w[class other_service],
34
+ commands: [{ args: ['police', 600_000_000, 1_000_000,
35
+ 'exceed-action',
36
+ 'policed-dscp-transmit'],
37
+ commands: [{ args: %w[set dscp cs2],
38
+ commands: [], pos: 214 },
39
+ { args: ['command_with_no_args'],
40
+ commands: [], pos: 230 }],
41
+ pos: 150 }],
42
+ pos: 128 }],
43
+ pos: 0 }]
44
+ }
45
+ end
46
+
47
+ describe '#call' do
48
+ subject { klass.new.call(input) }
49
+ let(:subject_pure) do
50
+ klass.new(lexer: IOSParser::PureLexer.new).call(input)
51
+ end
52
+
53
+ it('constructs the right AST') { expect(subject.to_hash).to eq output }
54
+
55
+ it('constructs the right AST (using the pure-ruby lexer)') do
56
+ expect(subject_pure.to_hash[:commands]).to eq output[:commands]
57
+ end
58
+
59
+ it('can be searched by an exact command') do
60
+ expect(subject.find_all(name: 'set').map(&:to_hash))
61
+ .to eq [{ args: %w[set dscp cs1],
62
+ commands: [], pos: 114 },
63
+ { args: %w[set dscp cs2],
64
+ commands: [], pos: 214 }]
65
+ end
66
+
67
+ context 'can be searched by name and the first argument' do
68
+ let(:result) do
69
+ expect(subject.find_all(starts_with: starts_with).map(&:to_hash))
70
+ .to eq expectation
71
+ end
72
+
73
+ let(:expectation) { [output[:commands][0][:commands][1]] }
74
+
75
+ context 'with an array of strings' do
76
+ let(:starts_with) { %w[class other_service] }
77
+ it { result }
78
+ end
79
+
80
+ context 'with an array of regular expressions' do
81
+ let(:starts_with) { [/.lass/, /^other_[a-z]+$/] }
82
+ it { result }
83
+ end
84
+
85
+ context 'with a string, space-separated' do
86
+ let(:starts_with) { 'class other_service' }
87
+ it { result }
88
+ end
89
+
90
+ context 'integer argument' do
91
+ let(:expectation) do
92
+ [{ args: ['police', 300_000_000, 1_000_000, 'exceed-action',
93
+ 'policed-dscp-transmit'],
94
+ commands: [{ args: %w[set dscp cs1],
95
+ commands: [], pos: 114 }],
96
+ pos: 50 }]
97
+ end
98
+
99
+ context 'integer query' do
100
+ let(:starts_with) { ['police', 300_000_000] }
101
+ it { result }
102
+ end # context 'integer query'
103
+
104
+ context 'string query' do
105
+ let(:starts_with) { 'police 300000000' }
106
+ it { result }
107
+ end # context 'string query'
108
+ end
109
+ end # context 'integer argument'
110
+
111
+ context 'nested search' do
112
+ it 'queries can be chained' do
113
+ expect(subject
114
+ .find('policy-map').find('class').find('police')
115
+ .find('set')
116
+ .to_hash)
117
+ .to eq(args: %w[set dscp cs1],
118
+ commands: [], pos: 114)
119
+ end
120
+ end # context 'nested search'
121
+
122
+ context 'pass a block' do
123
+ it 'is evaluated for each matching command' do
124
+ ary = []
125
+ subject.find_all('class') { |cmd| ary << cmd.args[1] }
126
+ expect(ary).to eq %w[myservice_service other_service]
127
+ end
128
+ end # context 'pass a block'
129
+ end # end context 'indented region'
130
+
131
+ context '2950' do
132
+ let(:input) { <<-END.unindent }
133
+ hostname myswitch1
134
+ vlan 3
135
+ name MyVlanName
136
+ interface FastEthernet0/1
137
+ speed 100
138
+ END
139
+
140
+ let(:output) { klass.new.call(input) }
141
+
142
+ it { expect(output.find('hostname').args[1]).to eq 'myswitch1' }
143
+
144
+ it('extracts vlan names') do
145
+ expect(output.find('vlan 3').find('name').args[1])
146
+ .to eq 'MyVlanName'
147
+ end
148
+
149
+ it('extracts interface speed') do
150
+ expect(output.find('interface FastEthernet0/1').find('speed').args[1])
151
+ .to eq 100
152
+ end
153
+
154
+ it('parses snmp commands') do
155
+ snmp_command = <<-END.unindent
156
+ snmp-server group my_group v3 auth read my_ro
157
+ END
158
+ result = klass.new.call(snmp_command)
159
+ expect(result[0].name).to eq 'snmp-server'
160
+ end
161
+
162
+ it('parses a simple alias') do
163
+ simple_alias = 'alias exec stat sh int | inc [1-9].*ignored|[1-9].*'\
164
+ "resets|Ethernet0|minute\n"
165
+ result = klass.new.call(simple_alias)
166
+ expect(result[0].name).to eq 'alias'
167
+ end
168
+
169
+ it('parses a complex alias') do
170
+ complex_alias = 'alias exec stats sh int | inc Ether.*'\
171
+ '(con|Loop.*is up|Vlan.*is up|Port-.*is up|'\
172
+ "input rate [^0]|output rate [^0]\n"
173
+ result = klass.new.call(complex_alias)
174
+ expect(result[0].name).to eq 'alias'
175
+ expect(result[0].args.last).to eq '[^0]'
176
+ end
177
+
178
+ it('parses a banner') do
179
+ banner_text = <<-END.unindent
180
+
181
+
182
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
183
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
184
+
185
+
186
+ END
187
+ banner_command = "banner exec ^C\n#{banner_text}^C\n"
188
+
189
+ result = klass.new.call(banner_command)
190
+ expect(result[0].args[2]).to eq banner_text
191
+
192
+ pure_result = klass.new(lexer: IOSParser::PureLexer.new)
193
+ .call(banner_command)
194
+ expect(pure_result[0].args[2]).to eq banner_text
195
+ end
196
+
197
+ it('parses a crypto trustpoint section') do
198
+ text = <<-END.unindent
199
+ crypto pki trustpoint TP-self-signed-0123456789
200
+ enrollment selfsigned
201
+ subject-name cn=IOS-Self-Signed-Certificate-1234567890
202
+ revocation-check none
203
+ rsakeypair TP-self-signed-2345678901
204
+ END
205
+ result = klass.new.call(text)
206
+ expect(result).not_to be_nil
207
+ end
208
+
209
+ it('parses a crypto certificate section') do
210
+ sp = ' '
211
+ text = <<-END.unindent
212
+ crypto pki certificate chain TP-self-signed-1234567890
213
+ certificate self-signed 01
214
+ FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF#{sp}
215
+ EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE#{sp}
216
+ DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD#{sp}
217
+ CCCCCCCC CCCCCCCC
218
+ quit
219
+
220
+ END
221
+
222
+ result = klass.new.call(text)
223
+ expect(result).not_to be_nil
224
+ end
225
+
226
+ it('parses an MST configuration section') do
227
+ text = <<-END.unindent
228
+ spanning-tree mst configuration
229
+ name MyMSTConfig
230
+ revision 1
231
+ instance 1 vlan 1-59, 4000
232
+ instance 2 vlan 90-99
233
+ instance 3 vlan 100-1500
234
+ instance 4 vlan 2000-3500, 4000
235
+ END
236
+
237
+ result = klass.new.call(text)
238
+ expect(result).not_to be_nil
239
+ end
240
+ end # context '2950'
241
+
242
+ it('finds various ip route formats') do
243
+ text = <<-END.unindent
244
+ ip route 10.0.0.1 255.255.255.255 Null0
245
+ ip route 9.9.9.199 255.255.255.255 42.42.42.142 name PONIES
246
+ ip route vrf Mgmt-intf 0.0.0.0 0.0.0.0 9.9.9.199
247
+ ip route 0.0.0.0/0 11.11.0.111 120
248
+ END
249
+
250
+ result = klass.new.call(text)
251
+
252
+ cmd_ary = [
253
+ { args: ['ip', 'route', '10.0.0.1', '255.255.255.255',
254
+ 'Null0'],
255
+ commands: [], pos: 0 },
256
+ { args: ['ip', 'route', '9.9.9.199', '255.255.255.255',
257
+ '42.42.42.142', 'name', 'PONIES'],
258
+ commands: [], pos: 40 },
259
+ { args: ['ip', 'route', 'vrf', 'Mgmt-intf', '0.0.0.0',
260
+ '0.0.0.0', '9.9.9.199'],
261
+ commands: [], pos: 100 },
262
+ { args: ['ip', 'route', '0.0.0.0/0', '11.11.0.111', 120],
263
+ commands: [], pos: 149 }
264
+ ]
265
+
266
+ expect(result.find_all('ip route').map(&:to_hash)).to eq(cmd_ary)
267
+
268
+ expect(result.find_all('ip route 9.9.9.199').length).to eq 1
269
+
270
+ cmd_hash = { args: ['ip', 'route', '9.9.9.199', '255.255.255.255',
271
+ '42.42.42.142', 'name', 'PONIES'],
272
+ commands: [], pos: 40 }
273
+ expect(result.find('ip route 9.9.9.199').to_hash).to eq(cmd_hash)
274
+ end # end context '#call'
275
+
276
+ describe '#to_s' do
277
+ subject { klass.new.call(input) }
278
+ let(:police2) { <<-END }
279
+ police 600000000 1000000 exceed-action policed-dscp-transmit
280
+ set dscp cs2
281
+ command_with_no_args
282
+ END
283
+
284
+ it('returns the string form of the original command(s)') do
285
+ expect(subject.to_s).to eq input
286
+ expect(subject.find('policy-map').to_s).to eq input
287
+ expect(subject.find('command_with_no_args').to_s)
288
+ .to eq " command_with_no_args\n"
289
+ expect(subject.find('police 600000000').to_s).to eq police2
290
+ end
291
+
292
+ context 'with dedent: true' do
293
+ it('returns the original without extra indentation') do
294
+ expect(subject.find('police 600000000').to_s(dedent: true))
295
+ .to eq police2.lines.map { |l| l[2..-1] }.join
296
+ end
297
+ end
298
+ end # describe '#to_s'
299
+
300
+ describe '#each' do
301
+ subject { klass.new.call(input) }
302
+ it 'traverses the AST' do
303
+ actual_paths = subject.map(&:path)
304
+ expected_paths = [
305
+ [],
306
+ ['policy-map mypolicy_in'],
307
+ ['policy-map mypolicy_in',
308
+ 'class myservice_service'],
309
+ ['policy-map mypolicy_in',
310
+ 'class myservice_service',
311
+ 'police 300000000 1000000 exceed-action policed-dscp-transmit'],
312
+ ['policy-map mypolicy_in'],
313
+ ['policy-map mypolicy_in',
314
+ 'class other_service'],
315
+ ['policy-map mypolicy_in',
316
+ 'class other_service',
317
+ 'police 600000000 1000000 exceed-action policed-dscp-transmit'],
318
+ ['policy-map mypolicy_in',
319
+ 'class other_service',
320
+ 'police 600000000 1000000 exceed-action policed-dscp-transmit']
321
+ ]
322
+ expect(actual_paths).to eq(expected_paths)
323
+ end
324
+ end # describe '#each'
325
+ end # context 'indented region'
326
+
327
+ context 'empty source' do
328
+ context 'when input is not a string' do
329
+ it 'raises ArgumentError' do
330
+ expect { klass.new.call [] }.to raise_error ArgumentError
331
+ expect { klass.new.call nil }.to raise_error ArgumentError
332
+ expect { klass.new.call 666 }.to raise_error ArgumentError
333
+ end
334
+ end # context 'when input is not a string' do
335
+ end # context 'empty source' do
336
+ end # end describe IOS
337
+ end # end module IOSParser
@@ -0,0 +1,290 @@
1
+ require_relative '../../spec_helper'
2
+ require 'ios_parser'
3
+ require 'ios_parser/lexer'
4
+
5
+ module IOSParser
6
+ describe Lexer do
7
+ describe '#call' do
8
+ subject { klass.new.call(input) }
9
+
10
+ let(:subject_pure) do
11
+ IOSParser::PureLexer.new.call(input)
12
+ end
13
+
14
+ context 'indented region' do
15
+ let(:input) { <<-END.unindent }
16
+ policy-map mypolicy_in
17
+ class myservice_service
18
+ police 300000000 1000000 exceed-action policed-dscp-transmit
19
+ set dscp cs1
20
+ class other_service
21
+ police 600000000 1000000 exceed-action policed-dscp-transmit
22
+ set dscp cs2
23
+ END
24
+
25
+ let(:output) do
26
+ ['policy-map', 'mypolicy_in', :EOL,
27
+ :INDENT,
28
+ 'class', 'myservice_service', :EOL,
29
+ :INDENT,
30
+ 'police', 300_000_000, 1_000_000, 'exceed-action',
31
+ 'policed-dscp-transmit', :EOL,
32
+ :INDENT,
33
+ 'set', 'dscp', 'cs1', :EOL,
34
+ :DEDENT, :DEDENT,
35
+ 'class', 'other_service', :EOL,
36
+ :INDENT,
37
+ 'police', 600_000_000, 1_000_000, 'exceed-action',
38
+ 'policed-dscp-transmit', :EOL,
39
+ :INDENT,
40
+ 'set', 'dscp', 'cs2', :EOL, :DEDENT, :DEDENT, :DEDENT]
41
+ end
42
+
43
+ subject { klass.new.call(input).map(&:last) }
44
+ it('enclosed in symbols') { should == output }
45
+
46
+ it('enclosed in symbols (using the pure ruby lexer)') do
47
+ expect(subject_pure.map(&:last)).to eq output
48
+ end
49
+ end
50
+
51
+ context 'ASR indented regions' do
52
+ context 'indented region' do
53
+ let(:input) { <<-END.unindent }
54
+ router static
55
+ vrf MGMT
56
+ address-family ipv4 unicast
57
+ 0.0.0.0/0 1.2.3.4
58
+ !
59
+ !
60
+ !
61
+ router ospf 12345
62
+ nsr
63
+ END
64
+
65
+ let(:expectation) do
66
+ ['router', 'static', :EOL,
67
+ :INDENT, 'vrf', 'MGMT', :EOL,
68
+ :INDENT, 'address-family', 'ipv4', 'unicast', :EOL,
69
+ :INDENT, '0.0.0.0/0', '1.2.3.4', :EOL,
70
+ :DEDENT, :DEDENT, :DEDENT,
71
+ 'router', 'ospf', 12_345, :EOL,
72
+ :INDENT, 'nsr', :EOL,
73
+ :DEDENT]
74
+ end
75
+
76
+ it 'pure' do
77
+ tokens = IOSParser::PureLexer.new.call(input)
78
+ expect(tokens.map(&:last)).to eq expectation
79
+ end # it 'pure' do
80
+
81
+ it 'default' do
82
+ tokens = IOSParser.lexer.new.call(input)
83
+ expect(tokens.map(&:last)).to eq expectation
84
+ end # it 'c' do
85
+ end # context 'indented region' do
86
+ end # context 'ASR indented regions' do
87
+
88
+ context 'banners' do
89
+ let(:input) do
90
+ <<-END.unindent
91
+ banner foobar ^
92
+ asdf 1234 9786 asdf
93
+ line 2
94
+ line 3
95
+ ^
96
+ END
97
+ end
98
+
99
+ let(:output) do
100
+ [[0, 'banner'], [7, 'foobar'], [14, :BANNER_BEGIN],
101
+ [16, "asdf 1234 9786 asdf\nline 2\nline 3\n "],
102
+ [52, :BANNER_END], [53, :EOL]]
103
+ end
104
+
105
+ it('tokenized and enclosed in symbols') { should == output }
106
+
107
+ it('tokenized and enclodes in symbols (using the pure ruby lexer)') do
108
+ expect(subject_pure).to eq output
109
+ end
110
+ end
111
+
112
+ context 'complex banner' do
113
+ let(:input) do
114
+ text_fixture('complex_banner')
115
+ end
116
+
117
+ let(:output) do
118
+ content = text_fixture('complex_banner').lines[1..-2].join
119
+ ['banner', 'exec', :BANNER_BEGIN, content, :BANNER_END, :EOL]
120
+ end
121
+
122
+ it { expect(subject.map(&:last)).to eq output }
123
+ it { expect(subject_pure.map(&:last)).to eq output }
124
+ end
125
+
126
+ context 'complex eos banner' do
127
+ let(:input) { "banner motd\n'''\nEOF\n" }
128
+
129
+ let(:output) do
130
+ content = input.lines[1..-2].join
131
+ ['banner', 'motd', :BANNER_BEGIN, content, :BANNER_END, :EOL]
132
+ end
133
+
134
+ it { expect(subject.map(&:last)).to eq output }
135
+ it { expect(subject_pure.map(&:last)).to eq output }
136
+ end
137
+
138
+ context 'decimal number' do
139
+ let(:input) { 'boson levels at 93.2' }
140
+ let(:output) { ['boson', 'levels', 'at', 93.2] }
141
+ subject { klass.new.call(input).map(&:last) }
142
+ it('converts to Float') { should == output }
143
+ end
144
+
145
+ context 'cryptographic certificate' do
146
+ let(:input) do
147
+ <<END.unindent
148
+ crypto pki certificate chain TP-self-signed-0123456789
149
+ certificate self-signed 01
150
+ FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
151
+ EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE
152
+ DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD AAAA
153
+ quit
154
+ !
155
+ END
156
+ end
157
+
158
+ let(:output) do
159
+ [[0, 'crypto'],
160
+ [7, 'pki'],
161
+ [11, 'certificate'],
162
+ [23, 'chain'],
163
+ [29, 'TP-self-signed-0123456789'],
164
+ [54, :EOL],
165
+ [56, :INDENT],
166
+ [56, 'certificate'],
167
+ [68, 'self-signed'],
168
+ [80, '01'],
169
+ [85, :CERTIFICATE_BEGIN],
170
+ [85,
171
+ 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF '\
172
+ 'FFFFFFFF EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE '\
173
+ 'EEEEEEEE EEEEEEEE DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD '\
174
+ 'DDDDDDDD DDDDDDDD DDDDDDDD AAAA'],
175
+ [323, :CERTIFICATE_END],
176
+ [323, :EOL],
177
+ [323, :DEDENT]]
178
+ end
179
+
180
+ subject { klass.new.call(input) }
181
+ it('tokenized') { expect(subject).to eq output }
182
+
183
+ it('tokenized (using the pure ruby lexer)') do
184
+ expect(subject_pure).to eq output
185
+ end
186
+ end
187
+
188
+ context 'comments' do
189
+ let(:input) { 'ip addr 127.0.0.0.1 ! asdfsdf' }
190
+ let(:output) { ['ip', 'addr', '127.0.0.0.1'] }
191
+ subject { klass.new.call(input).map(&:last) }
192
+ it('dropped') { should == output }
193
+ end
194
+
195
+ context 'quoted octothorpe' do
196
+ let(:input) { <<-EOS.unindent }
197
+ vlan 1
198
+ name "a #"
199
+ vlan 2
200
+ name d
201
+ EOS
202
+
203
+ let(:output) do
204
+ [
205
+ 'vlan', 1, :EOL,
206
+ :INDENT, 'name', '"a #"', :EOL,
207
+ :DEDENT,
208
+ 'vlan', 2, :EOL,
209
+ :INDENT, 'name', 'd', :EOL,
210
+ :DEDENT
211
+ ]
212
+ end
213
+
214
+ it { expect(subject_pure.map(&:last)).to eq output }
215
+ it { expect(subject.map(&:last)).to eq output }
216
+ end # context 'quoted octothorpe' do
217
+
218
+ context 'vlan range' do
219
+ let(:input) { 'switchport trunk allowed vlan 50-90' }
220
+ let(:output) do
221
+ [
222
+ [0, 'switchport'],
223
+ [11, 'trunk'],
224
+ [17, 'allowed'],
225
+ [25, 'vlan'],
226
+ [30, '50-90']
227
+ ]
228
+ end
229
+ it { should == output }
230
+ end # context 'vlan range' do
231
+
232
+ context 'partial dedent' do
233
+ let(:input) do
234
+ <<END.unindent
235
+ class-map match-any foobar
236
+ description blahblahblah
237
+ match access-group fred
238
+ END
239
+ end
240
+
241
+ let(:output) do
242
+ [
243
+ 'class-map', 'match-any', 'foobar', :EOL,
244
+ :INDENT, 'description', 'blahblahblah', :EOL,
245
+ 'match', 'access-group', 'fred', :EOL,
246
+ :DEDENT
247
+ ]
248
+ end
249
+
250
+ it { expect(subject_pure.map(&:last)).to eq output }
251
+ end
252
+
253
+ context '# in the middle of a line is not a comment' do
254
+ let(:input) { "vlan 1\n name #31337" }
255
+ let(:output) { ['vlan', 1, :EOL, :INDENT, 'name', '#31337', :DEDENT] }
256
+
257
+ it { expect(subject_pure.map(&:last)).to eq output }
258
+ it { expect(subject.map(&:last)).to eq output }
259
+ end
260
+
261
+ context '# at the start of a line is a comment' do
262
+ let(:input) { "vlan 1\n# comment\nvlan 2" }
263
+ let(:output) { ['vlan', 1, :EOL, 'vlan', 2] }
264
+
265
+ it { expect(subject_pure.map(&:last)).to eq output }
266
+ it { expect(subject.map(&:last)).to eq output }
267
+ end
268
+
269
+ context '# after indentation is a comment' do
270
+ let(:input) { "vlan 1\n # comment\nvlan 2" }
271
+ let(:output) { ['vlan', 1, :EOL, :INDENT, :DEDENT, 'vlan', 2] }
272
+
273
+ it { expect(subject_pure.map(&:last)).to eq output }
274
+ it { expect(subject.map(&:last)).to eq output }
275
+ end
276
+
277
+ context 'unterminated quoted string' do
278
+ let(:input) { '"asdf' }
279
+ it 'raises a lex error' do
280
+ expect { subject_pure }.to raise_error IOSParser::LexError
281
+ expect { subject }.to raise_error IOSParser::LexError
282
+
283
+ pattern = /Unterminated quoted string starting at 0: #{input}/
284
+ expect { subject_pure }.to raise_error(pattern)
285
+ expect { subject }.to raise_error(pattern)
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end