ios_parser 0.5.1-java

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.
@@ -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