unobtainium-cucumber 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,115 @@
1
+ Feature:
2
+ Provide the ability to conveniently perform actions when a scenario finishes
3
+ and its status is known.
4
+
5
+ Background:
6
+ Given I have a test instance of the StatusActions module
7
+ Then I expect all configured actions to be present
8
+ And I want to clear configured actions
9
+
10
+ Scenario Outline: Function `#action_key`
11
+ Given I have a scenario which has <has_passed>
12
+ And the scenario <is_outline> an outline
13
+ Then I expect the output to contain <status> and <type>
14
+
15
+ Examples:
16
+ | has_passed | is_outline | status | type |
17
+ | passed | is not | :passed? | :scenario |
18
+ | failed | is not | :failed? | :scenario |
19
+ | passed | is | :passed? | :outline |
20
+ | failed | is | :failed? | :outline |
21
+
22
+ Scenario Outline: Function `#register_action` status
23
+ Given I try to register an action for status <status>
24
+ Then I expect the function to <succeed_or_fail>
25
+
26
+ Examples:
27
+ | status | succeed_or_fail |
28
+ | passed? | succeed |
29
+ | failed? | succeed |
30
+ | passed | fail |
31
+ | | fail |
32
+
33
+ Scenario Outline: Function `#register_action` type option
34
+ Given I try to register an action for the type <type>
35
+ Then I expect the function to <succeed_or_fail>
36
+
37
+ Examples:
38
+ | type | succeed_or_fail |
39
+ | scenario | succeed |
40
+ | outline | succeed |
41
+ | other | fail |
42
+ | | fail |
43
+
44
+ Scenario: Try to register neither function nor block
45
+ Given I try to register no action
46
+ Then I expect the function to fail
47
+
48
+ Scenario: Try to register both function and block
49
+ Given I try to register two actions
50
+ Then I expect the function to fail
51
+
52
+ Scenario: No action, but options
53
+ Given I register no action, but provide options
54
+ Then I expect the there to be an error
55
+
56
+ Scenario: Invalid Hash action
57
+ Given I register a Hash as an action
58
+ Then I expect the there to be an error
59
+
60
+ Scenario: Multiple actions
61
+ Given I register an action
62
+ When I register another action
63
+ Then I expect there to be two registered actions
64
+
65
+ Scenario: No actions
66
+ Given I have registered no actions
67
+ Then I expect there to be no registered actions
68
+
69
+ Scenario: Action is executed
70
+ Given I have registered an action
71
+ Then I expect it to be executed
72
+
73
+ Scenario: Configured actions should be present
74
+ Given I register configured actions
75
+ Then I expect only configured actions to be present
76
+
77
+ Scenario: Execute method
78
+ Given I execute a method action
79
+ Then I expect this to succeed
80
+
81
+ Scenario: Execute block
82
+ Given I execute a block action
83
+ Then I expect this to succeed
84
+
85
+ Scenario: Execute symbol
86
+ Given I execute a symbol action
87
+ Then I expect this to succeed
88
+
89
+ Scenario: Execute string
90
+ Given I execute a string action
91
+ Then I expect this to succeed
92
+
93
+ Scenario: Execute namespaced string
94
+ Given I execute a namespaced string action
95
+ Then I expect this to succeed
96
+
97
+ Scenario: Execute dot-separated namespaced string
98
+ Given I execute a namespaced string action that is dot-separated
99
+ Then I expect this to succeed
100
+
101
+ Scenario: Execute non-resolving string
102
+ Given I execute string action that does not resolve
103
+ Then I expect this to fail
104
+
105
+ Scenario: Execute non-resolving symbol
106
+ Given I execute symbol action that does not resolve
107
+ Then I expect this to fail
108
+
109
+ Scenario: Execute non-resolving string with two dots
110
+ Given I execute string action with two dots
111
+ Then I expect this to fail
112
+
113
+ Scenario: Try to execute number
114
+ Given I execute a number action
115
+ Then I expect this to fail
@@ -0,0 +1,61 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ require 'unobtainium-cucumber/action/screenshot'
11
+ require 'unobtainium-cucumber/action/content'
12
+ require_relative './mocks/scenario'
13
+
14
+ Given(/^I take a screenshot$/) do
15
+ ::Unobtainium::Cucumber::Action.store_screenshot(
16
+ self, MockScenario.new('screenshots')
17
+ )
18
+ end
19
+
20
+ Then(/^I expect there to be a matching screenshot file$/) do
21
+ # The expectation is that the file starts with an ISO8601 timestamp of today
22
+ # and ends in 'screenshots.png'. The part we can't be certain about is the
23
+ # exact time stamp, because seconds, minutes, etc. even years could have
24
+ # rolled over between taking the screenshot and finding it.
25
+
26
+ # So what we do instead is find files that match the end. If the start matches
27
+ # the syntax of a timestamp string, we can convert it to a timestamp. Then we
28
+ # can find out if between said timestamp and right now only a few seconds
29
+ # elapsed. If we find one such file, the test succeeds.
30
+ pattern = 'screenshots/*-screenshots.png'
31
+ timeout_match_files(pattern)
32
+
33
+ # *After* all checks, remove matching files.
34
+ FileUtils.rm(Dir.glob(pattern))
35
+ end
36
+
37
+ Given(/^I navigate to the best site in the world$/) do
38
+ driver.navigate.to 'http://finkhaeuser.de'
39
+ end
40
+
41
+ When(/^I capture the page content$/) do
42
+ # See the similar screnshot matching step for some details.
43
+ ::Unobtainium::Cucumber::Action.store_content(
44
+ self, MockScenario.new('contents')
45
+ )
46
+ end
47
+
48
+ Then(/^I expect there to be a matching content file$/) do
49
+ # See the similar screnshot matching step for some details.
50
+ pattern = 'content/*-contents.txt'
51
+ match_file = timeout_match_files(pattern)
52
+
53
+ # Check that some expected content exists.
54
+ match = File.open(match_file).grep(/<html/)
55
+ if match.empty?
56
+ raise "File content of '#{match_file}' does not seem to be valid HTML!"
57
+ end
58
+
59
+ # *After* all checks, remove matching files.
60
+ FileUtils.rm(Dir.glob(pattern))
61
+ end
@@ -0,0 +1,40 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ require 'unobtainium-cucumber/action/support/naming.rb'
11
+ require_relative './mocks/scenario'
12
+
13
+ Given(/^I have a scenario named (.+)$/) do |scenario|
14
+ @action_support = {}
15
+ @action_support[:scenario] = MockScenario.new(scenario)
16
+ end
17
+
18
+ Given(/^I provide a tag (.+)$/) do |tag|
19
+ if tag == 'NIL'
20
+ tag = nil
21
+ end
22
+ @action_support[:tag] = tag
23
+ end
24
+
25
+ Given(/^the timestamp is (\d+\-\d+\-\d+T\d+:\d+:\d+Z)$/) do |timestamp|
26
+ @action_support[:timestamp] = timestamp
27
+ end
28
+
29
+ Then(/^I expect the filename to match (.+)/) do |expected|
30
+ expected = Regexp.new(expected)
31
+
32
+ tester = Class.new { extend ::Unobtainium::Cucumber::Action::Support }
33
+ result = tester.base_filename(@action_support[:scenario],
34
+ @action_support[:tag],
35
+ @action_support[:timestamp])
36
+
37
+ if not expected.match(result)
38
+ raise "Result '#{result}' did not match expectation #{expected}!"
39
+ end
40
+ end
@@ -0,0 +1,85 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ # rubocop:disable Style/GlobalVars
11
+ $reset_called = 0
12
+
13
+ def define_reset(the_driver = driver)
14
+ the_driver.class.class_eval do
15
+ def reset(*args)
16
+ $reset_called += 1
17
+ # rubocop:disable Lint/HandleExceptions
18
+ begin
19
+ super(*args)
20
+ rescue NoMethodError
21
+ # Work around drivers not supporting reset
22
+ end
23
+ # rubocop:enable Lint/HandleExceptions
24
+ end
25
+ end
26
+ end
27
+
28
+ def undefine_reset(the_driver = driver)
29
+ the_driver.class.class_eval do
30
+ # rubocop:disable Lint/HandleExceptions
31
+ begin
32
+ remove_method :reset
33
+ rescue NameError
34
+ end
35
+ # rubocop:enable Lint/HandleExceptions
36
+ end
37
+ end
38
+
39
+ Given(/^I remove any reset functions$/) do
40
+ undefine_reset
41
+ end
42
+
43
+ Given(/^I run a scenario to test driver reset$/) do
44
+ # We decorate the driver class with a 'reset' method for two reasons:
45
+ # a) we can this way track that it was called, and
46
+ # b) we work around the fact that not all drivers support the 'reset'
47
+ # function.
48
+ define_reset
49
+ end
50
+
51
+ Then(/^the driver should be reset at the end$/) do
52
+ # Absolutely nothing to do here; but see $reset_called
53
+ end
54
+
55
+ Given(/^I run a scenario with a driver that knowns no reset$/) do
56
+ # Instanciate the driver by looking at its class.
57
+ driver.class
58
+ end
59
+
60
+ Then(/^the driver should not be reset at the end$/) do
61
+ # Absolutely nothing to do here; but see $reset_called
62
+ end
63
+
64
+ Given(/^I run a scenario with driver reset switched off$/) do
65
+ define_reset
66
+ # Note: enable this line, and $reset_called should be *zero* at the end.
67
+ # Testing this is really, really awkward.
68
+ # config['cucumber.driver_reset'] = false
69
+ end
70
+
71
+ at_exit do
72
+ # We expect reset to be called exactly twice, for the driver that defines
73
+ # such a function. But see the scenario for switching off driver reset
74
+ # above.
75
+ if not $reset_called >= 2
76
+ warn '*' * 80
77
+ warn "* If this fails, check the steps in 'driver_reset.feature'!"
78
+ warn "* We expected reset to be twice or more, but it got called "\
79
+ "#{$reset_called} times!"
80
+ warn '*' * 80
81
+ Kernel.exit(3)
82
+ end
83
+ end
84
+
85
+ # rubocop:enable Style/GlobalVars
@@ -0,0 +1,19 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ Given(/^I register an extension with cucumber's World$/) do
11
+ # There's nothing to do here. The extension is registered in
12
+ # features/support/env.rb as it should be.
13
+ end
14
+
15
+ Then(/^I expect this extension to be used$/) do
16
+ # Calling the method should already fail, so no need for testing whether
17
+ # the method is callable :)
18
+ method_from_own_extension
19
+ end
@@ -0,0 +1,18 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ ##
11
+ # A mock scenario has a name
12
+ class MockScenario
13
+ def initialize(name)
14
+ @name = name
15
+ end
16
+
17
+ attr_reader :name
18
+ end
@@ -0,0 +1,358 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ # rubocop:disable Style/GlobalVars
11
+
12
+ def dummy_action(*_); end
13
+
14
+ def global_action(*_); end
15
+
16
+ $counter = 0
17
+ def counting_action(*_)
18
+ $counter += 1
19
+ end
20
+
21
+ def recording_action(_, scenario)
22
+ scenario.called = true
23
+ end
24
+
25
+ # TestModule
26
+ module TestModule
27
+ def self.inner_recorder(*args)
28
+ recording_action(*args)
29
+ end
30
+ end
31
+
32
+ # StatusActionsTester doubles both as a test target for the actions, a
33
+ # state crossing multiple step definitions, and behaving a bit like
34
+ # a scenario.
35
+ class StatusActionsTester
36
+ # Include StatusActions functionality...
37
+ include ::Unobtainium::Cucumber::StatusActions
38
+
39
+ # ... but also double as scenario
40
+ def passed?
41
+ return @passed
42
+ end
43
+
44
+ def outline?
45
+ return @outline
46
+ end
47
+
48
+ # Also, add setters for the above.
49
+ attr_writer :passed
50
+ attr_writer :outline
51
+
52
+ # Accessors for various test states
53
+ attr_accessor :register_status
54
+ attr_accessor :register_type
55
+ attr_accessor :register_functions
56
+
57
+ attr_accessor :exception
58
+
59
+ attr_accessor :called
60
+ end
61
+
62
+ Given(/^I have a test instance of the StatusActions module$/) do
63
+ @status_actions = StatusActionsTester.new
64
+ end
65
+
66
+ Then(/^I want to clear configured actions$/) do
67
+ @status_actions.clear_actions
68
+ end
69
+
70
+ Given(/^I have a scenario which has (passed|failed)$/) do |result|
71
+ @status_actions.passed = result == 'passed' ? true : false
72
+ end
73
+
74
+ Given(/^the scenario is( not)? an outline$/) do |negation|
75
+ @status_actions.outline = negation.nil? ? true : false
76
+ end
77
+
78
+ Then(
79
+ /^I expect the output to contain :(passed\?|failed\?) and :(scenario|outline)$/
80
+ ) do |status, type|
81
+ # Since the status action instance behaves as a scenario from the previous
82
+ # steps, we can pass it to the action_key method.
83
+ result = @status_actions.action_key(@status_actions)
84
+
85
+ expectation = [status.to_sym, type.to_sym]
86
+ if result != expectation
87
+ raise "Got '#{result}' but expected '#{expectation}'!"
88
+ end
89
+ end
90
+
91
+ Given(/^I try to register an action for status (.*)$/) do |status|
92
+ @status_actions.register_status = status.strip
93
+ @status_actions.register_type = :scenario # valid
94
+ @status_actions.register_functions = [:dummy_action, nil] # valid
95
+ end
96
+
97
+ Given(/^I try to register an action for the type (.*)$/) do |type|
98
+ @status_actions.register_status = :passed? # valid
99
+ @status_actions.register_type = type.strip
100
+ @status_actions.register_functions = [:dummy_action, nil] # valid
101
+ end
102
+
103
+ Given(/^I try to register no action$/) do
104
+ @status_actions.register_status = :passed? # valid
105
+ @status_actions.register_type = :scenario # valid
106
+ @status_actions.register_functions = [nil, nil]
107
+ end
108
+
109
+ Given(/^I try to register two actions$/) do
110
+ @status_actions.register_status = :passed? # valid
111
+ @status_actions.register_type = :scenario # valid
112
+ @status_actions.register_functions = [:dummy_action, proc { |*args| }]
113
+ end
114
+
115
+ Then(/^I expect the function to (.+)$/) do |result|
116
+ result.strip!
117
+
118
+ status_sym = @status_actions.register_status.to_sym
119
+ type_sym = @status_actions.register_type.to_sym
120
+ action_func = @status_actions.register_functions[0]
121
+ action_block = @status_actions.register_functions[1]
122
+
123
+ begin
124
+ @status_actions.register_action(status_sym, action_func, type: type_sym,
125
+ &action_block)
126
+ rescue RuntimeError => err
127
+ if result == 'succeed'
128
+ raise err
129
+ end
130
+ else
131
+ if result == 'fail'
132
+ raise "Expected this to fail, but it didn't!"
133
+ end
134
+ end
135
+ end
136
+
137
+ Given(/^I register no action, but provide options$/) do
138
+ # We'll perform the call, and record if there was an exception
139
+ begin
140
+ @status_actions.register_action(:passed?, type: :scenario)
141
+ rescue RuntimeError => err
142
+ @status_actions.exception = err
143
+ else
144
+ @status_actions.exception = nil
145
+ end
146
+ end
147
+
148
+ Given(/^I register a Hash as an action$/) do
149
+ # We'll perform the call, and record if there was an exception
150
+ begin
151
+ # Note that this is possible to do, but shouldn't happen. Normally
152
+ # if you pass a Hash as the second parmeter, you'd expect that to be
153
+ # the options, and a block to be provided.
154
+ @status_actions.register_action(:passed?, {}, {})
155
+ rescue RuntimeError => err
156
+ @status_actions.exception = err
157
+ else
158
+ @status_actions.exception = nil
159
+ end
160
+ end
161
+
162
+ Then(/^I expect the there to be an error$/) do
163
+ # Named differently from "the function to fail" because the above step would
164
+ # get far too complex.
165
+
166
+ # We expect an exception to be recorded, and if there is none, we fail
167
+ if @status_actions.exception.nil?
168
+ raise "Expected this to fail, but it didn't!"
169
+ end
170
+ end
171
+
172
+ Given(/^I register an action$/) do
173
+ @status_actions.register_action(:passed?, :dummy_action)
174
+ end
175
+
176
+ When(/^I register another action$/) do
177
+ @status_actions.register_action(:passed?) do |*args|
178
+ end
179
+ end
180
+
181
+ Then(/^I expect there to be two registered actions$/) do
182
+ # The default type is :scenario
183
+ registered = @status_actions.registered_actions(:passed?, :scenario)
184
+ if registered.length != 2
185
+ raise "Expected two registered actions, but found: #{registered.lenght}"
186
+ end
187
+ end
188
+
189
+ Given(/^I have registered no actions$/) do
190
+ # Not doing anything here!
191
+ end
192
+
193
+ Then(/^I expect there to be no registered actions$/) do
194
+ # The default type is :scenario
195
+ registered = @status_actions.registered_actions(:passed?, :scenario)
196
+ if not registered.empty?
197
+ raise "Expected zero registered actions, but found: #{registered.lenght}"
198
+ end
199
+ end
200
+
201
+ Given(/^I have registered an action$/) do
202
+ @status_actions.register_action(:passed?, :counting_action)
203
+ end
204
+
205
+ Then(/^I expect it to be executed$/) do
206
+ # Nothing to do; see at_exit below
207
+ end
208
+
209
+ Given(/^I register configured actions$/) do
210
+ # Configured actions should be registered automatically; that's hard to
211
+ # test here. However, see the background step!
212
+ # Let's just register them again!
213
+ register_config_actions(self)
214
+ end
215
+
216
+ Then(/^I expect all configured actions to be present$/) do
217
+ registered = @status_actions.registered_actions(:passed?, :scenario)
218
+ if not registered.include?('global_action')
219
+ raise "Expected 'global_action' in #{registered}!"
220
+ end
221
+
222
+ registered = @status_actions.registered_actions(:passed?, :outline)
223
+ if not registered.include?('global_action')
224
+ raise "Expected 'global_action' in #{registered}!"
225
+ end
226
+
227
+ registered = @status_actions.registered_actions(:failed?, :scenario)
228
+ if not registered.include?('method_from_own_extension')
229
+ raise "Expected 'global_action' in #{registered}!"
230
+ end
231
+
232
+ registered = @status_actions.registered_actions(:failed?, :outline)
233
+ if not registered.include?('dummy_action')
234
+ raise "Expected 'global_action' in #{registered}!"
235
+ end
236
+ end
237
+
238
+ Then(/^I expect only configured actions to be present$/) do
239
+ registered = @status_actions.registered_actions(:passed?, :scenario)
240
+ if registered != %w(global_action)
241
+ raise "Expected different actions than #{registered}!"
242
+ end
243
+
244
+ registered = @status_actions.registered_actions(:passed?, :outline)
245
+ if registered != %w(global_action)
246
+ raise "Expected different actions than #{registered}!"
247
+ end
248
+
249
+ registered = @status_actions.registered_actions(:failed?, :scenario)
250
+ if registered != %w(method_from_own_extension)
251
+ raise "Expected different actions than #{registered}!"
252
+ end
253
+
254
+ registered = @status_actions.registered_actions(:failed?, :outline)
255
+ if registered != %w(dummy_action)
256
+ raise "Expected different actions than #{registered}!"
257
+ end
258
+ end
259
+
260
+ Given(/^I execute a method action$/) do
261
+ @status_actions.called = false
262
+ @status_actions.execute_action(self, method(:recording_action), @status_actions)
263
+ end
264
+
265
+ Then(/^I expect this to succeed$/) do
266
+ if not @status_actions.called
267
+ raise "Expected the action to be invoked, but it wasn't!"
268
+ end
269
+ end
270
+
271
+ Given(/^I execute a block action$/) do
272
+ action = proc do |_, scenario|
273
+ scenario.called = true
274
+ end
275
+
276
+ @status_actions.called = false
277
+ @status_actions.execute_action(self, action, @status_actions)
278
+ end
279
+
280
+ Given(/^I execute a symbol action$/) do
281
+ @status_actions.called = false
282
+ @status_actions.execute_action(self, :recording_action, @status_actions)
283
+ end
284
+
285
+ Given(/^I execute a string action$/) do
286
+ @status_actions.called = false
287
+ @status_actions.execute_action(self, 'recording_action', @status_actions)
288
+ end
289
+
290
+ Given(/^I execute a namespaced string action$/) do
291
+ @status_actions.called = false
292
+ @status_actions.execute_action(self, '::TestModule::inner_recorder',
293
+ @status_actions)
294
+ end
295
+
296
+ Given(/^I execute a namespaced string action that is dot\-separated$/) do
297
+ @status_actions.called = false
298
+ @status_actions.execute_action(self, '::TestModule.inner_recorder',
299
+ @status_actions)
300
+ end
301
+
302
+ Given(/^I execute string action that does not resolve$/) do
303
+ @status_actions.called = false
304
+ # rubocop:disable Lint/HandleExceptions
305
+ begin
306
+ @status_actions.execute_action(self, 'does not resolve', @status_actions)
307
+ rescue => _err
308
+ end
309
+ # rubocop:enable Lint/HandleExceptions
310
+ end
311
+
312
+ Given(/^I execute symbol action that does not resolve$/) do
313
+ @status_actions.called = false
314
+ # rubocop:disable Lint/HandleExceptions
315
+ begin
316
+ @status_actions.execute_action(self, :does_not_resolve, @status_actions)
317
+ rescue => _err
318
+ end
319
+ # rubocop:enable Lint/HandleExceptions
320
+ end
321
+
322
+ Given(/^I execute string action with two dots$/) do
323
+ @status_actions.called = false
324
+ # rubocop:disable Lint/HandleExceptions
325
+ begin
326
+ @status_actions.execute_action(self, 'can.not.resolve', @status_actions)
327
+ rescue => _err
328
+ end
329
+ # rubocop:enable Lint/HandleExceptions
330
+ end
331
+
332
+ Given(/^I execute a number action$/) do
333
+ @status_actions.called = false
334
+ # rubocop:disable Lint/HandleExceptions
335
+ begin
336
+ @status_actions.execute_action(self, 42, @status_actions)
337
+ rescue => _err
338
+ end
339
+ # rubocop:enable Lint/HandleExceptions
340
+ end
341
+
342
+ Then(/^I expect this to fail$/) do
343
+ if @status_actions.called
344
+ raise "Expected the action not to be invoked, but it was!"
345
+ end
346
+ end
347
+
348
+ at_exit do
349
+ # We expect the counting_action to be called once.
350
+ if not $counter >= 1
351
+ warn '*' * 80
352
+ warn "* If this fails, check the steps in 'status_actions.feature'!"
353
+ warn '*' * 80
354
+ Kernel.exit(4)
355
+ end
356
+ end
357
+
358
+ # rubocop:enable Style/GlobalVars