airbrake-ruby 4.8.0 → 4.10.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/lib/airbrake-ruby.rb +84 -5
- data/lib/airbrake-ruby/async_sender.rb +3 -3
- data/lib/airbrake-ruby/backtrace.rb +2 -2
- data/lib/airbrake-ruby/code_hunk.rb +1 -1
- data/lib/airbrake-ruby/config.rb +1 -1
- data/lib/airbrake-ruby/config/validator.rb +3 -3
- data/lib/airbrake-ruby/deploy_notifier.rb +1 -1
- data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +2 -2
- data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +2 -2
- data/lib/airbrake-ruby/filters/keys_filter.rb +1 -1
- data/lib/airbrake-ruby/filters/sql_filter.rb +3 -3
- data/lib/airbrake-ruby/filters/thread_filter.rb +1 -1
- data/lib/airbrake-ruby/grouppable.rb +12 -0
- data/lib/airbrake-ruby/inspectable.rb +2 -2
- data/lib/airbrake-ruby/mergeable.rb +12 -0
- data/lib/airbrake-ruby/notice.rb +7 -7
- data/lib/airbrake-ruby/notice_notifier.rb +1 -1
- data/lib/airbrake-ruby/performance_breakdown.rb +2 -1
- data/lib/airbrake-ruby/performance_notifier.rb +42 -21
- data/lib/airbrake-ruby/query.rb +3 -5
- data/lib/airbrake-ruby/queue.rb +52 -0
- data/lib/airbrake-ruby/request.rb +3 -5
- data/lib/airbrake-ruby/stat.rb +1 -1
- data/lib/airbrake-ruby/version.rb +1 -1
- data/spec/airbrake_spec.rb +135 -45
- data/spec/async_sender_spec.rb +4 -4
- data/spec/backtrace_spec.rb +18 -18
- data/spec/code_hunk_spec.rb +9 -9
- data/spec/config/validator_spec.rb +5 -5
- data/spec/config_spec.rb +5 -5
- data/spec/deploy_notifier_spec.rb +2 -2
- data/spec/filter_chain_spec.rb +1 -1
- data/spec/filters/dependency_filter_spec.rb +1 -1
- data/spec/filters/gem_root_filter_spec.rb +5 -5
- data/spec/filters/git_last_checkout_filter_spec.rb +1 -1
- data/spec/filters/git_repository_filter.rb +1 -1
- data/spec/filters/git_revision_filter_spec.rb +10 -10
- data/spec/filters/keys_blacklist_spec.rb +22 -22
- data/spec/filters/keys_whitelist_spec.rb +21 -21
- data/spec/filters/root_directory_filter_spec.rb +5 -5
- data/spec/filters/sql_filter_spec.rb +53 -53
- data/spec/filters/system_exit_filter_spec.rb +1 -1
- data/spec/filters/thread_filter_spec.rb +28 -28
- data/spec/fixtures/project_root/code.rb +9 -9
- data/spec/notice_notifier/options_spec.rb +12 -12
- data/spec/notice_notifier_spec.rb +17 -17
- data/spec/notice_spec.rb +5 -5
- data/spec/performance_notifier_spec.rb +171 -60
- data/spec/query_spec.rb +1 -1
- data/spec/queue_spec.rb +11 -0
- data/spec/request_spec.rb +1 -1
- data/spec/response_spec.rb +8 -8
- data/spec/spec_helper.rb +2 -2
- data/spec/stat_spec.rb +2 -2
- data/spec/sync_sender_spec.rb +12 -12
- data/spec/tdigest_spec.rb +6 -6
- data/spec/thread_pool_spec.rb +5 -5
- data/spec/timed_trace_spec.rb +1 -1
- data/spec/truncator_spec.rb +12 -12
- metadata +7 -2
@@ -12,6 +12,7 @@ module Airbrake
|
|
12
12
|
include HashKeyable
|
13
13
|
include Ignorable
|
14
14
|
include Stashable
|
15
|
+
include Mergeable
|
15
16
|
|
16
17
|
def initialize(
|
17
18
|
method:,
|
@@ -38,7 +39,7 @@ module Airbrake
|
|
38
39
|
'method' => method,
|
39
40
|
'route' => route,
|
40
41
|
'responseType' => response_type,
|
41
|
-
'time' => @start_time_utc
|
42
|
+
'time' => @start_time_utc,
|
42
43
|
}.delete_if { |_key, val| val.nil? }
|
43
44
|
end
|
44
45
|
end
|
@@ -4,6 +4,7 @@ module Airbrake
|
|
4
4
|
#
|
5
5
|
# @api public
|
6
6
|
# @since v3.2.0
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
7
8
|
class PerformanceNotifier
|
8
9
|
include Inspectable
|
9
10
|
include Loggable
|
@@ -11,7 +12,8 @@ module Airbrake
|
|
11
12
|
def initialize
|
12
13
|
@config = Airbrake::Config.instance
|
13
14
|
@flush_period = Airbrake::Config.instance.performance_stats_flush_period
|
14
|
-
@
|
15
|
+
@async_sender = AsyncSender.new(:put)
|
16
|
+
@sync_sender = SyncSender.new(:put)
|
15
17
|
@payload = {}
|
16
18
|
@schedule_flush = nil
|
17
19
|
@mutex = Mutex.new
|
@@ -23,21 +25,14 @@ module Airbrake
|
|
23
25
|
# @see Airbrake.notify_query
|
24
26
|
# @see Airbrake.notify_request
|
25
27
|
def notify(resource)
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
promise = @config.check_performance_options(resource)
|
30
|
-
return promise if promise.rejected?
|
31
|
-
|
32
|
-
@filter_chain.refine(resource)
|
33
|
-
return if resource.ignored?
|
34
|
-
|
35
|
-
@mutex.synchronize do
|
36
|
-
update_payload(resource)
|
37
|
-
@flush_period > 0 ? schedule_flush : send(@payload, promise)
|
38
|
-
end
|
28
|
+
send_resource(resource, sync: false)
|
29
|
+
end
|
39
30
|
|
40
|
-
|
31
|
+
# @param [Hash] resource
|
32
|
+
# @since v4.10.0
|
33
|
+
# @see Airbrake.notify_queue_sync
|
34
|
+
def notify_sync(resource)
|
35
|
+
send_resource(resource, sync: true).value
|
41
36
|
end
|
42
37
|
|
43
38
|
# @see Airbrake.add_performance_filter
|
@@ -53,7 +48,7 @@ module Airbrake
|
|
53
48
|
def close
|
54
49
|
@mutex.synchronize do
|
55
50
|
@schedule_flush.kill if @schedule_flush
|
56
|
-
@
|
51
|
+
@async_sender.close
|
57
52
|
logger.debug("#{LOG_LABEL} performance notifier closed")
|
58
53
|
end
|
59
54
|
end
|
@@ -61,7 +56,12 @@ module Airbrake
|
|
61
56
|
private
|
62
57
|
|
63
58
|
def update_payload(resource)
|
64
|
-
@payload[resource]
|
59
|
+
if (total_stat = @payload[resource])
|
60
|
+
@payload.key(total_stat).merge(resource)
|
61
|
+
else
|
62
|
+
@payload[resource] = { total: Airbrake::Stat.new }
|
63
|
+
end
|
64
|
+
|
65
65
|
@payload[resource][:total].increment(resource.start_time, resource.end_time)
|
66
66
|
|
67
67
|
resource.groups.each do |name, ms|
|
@@ -101,12 +101,32 @@ module Airbrake
|
|
101
101
|
@payload = {}
|
102
102
|
end
|
103
103
|
|
104
|
-
send(payload, Airbrake::Promise.new)
|
104
|
+
send(@async_sender, payload, Airbrake::Promise.new)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def send_resource(resource, sync:)
|
110
|
+
promise = @config.check_configuration
|
111
|
+
return promise if promise.rejected?
|
112
|
+
|
113
|
+
promise = @config.check_performance_options(resource)
|
114
|
+
return promise if promise.rejected?
|
115
|
+
|
116
|
+
@filter_chain.refine(resource)
|
117
|
+
return if resource.ignored?
|
118
|
+
|
119
|
+
@mutex.synchronize do
|
120
|
+
update_payload(resource)
|
121
|
+
if sync || @flush_period == 0
|
122
|
+
send(@sync_sender, @payload, promise)
|
123
|
+
else
|
124
|
+
schedule_flush
|
105
125
|
end
|
106
126
|
end
|
107
127
|
end
|
108
128
|
|
109
|
-
def send(payload, promise)
|
129
|
+
def send(sender, payload, promise)
|
110
130
|
signature = "#{self.class.name}##{__method__}"
|
111
131
|
raise "#{signature}: payload (#{payload}) cannot be empty. Race?" if payload.none?
|
112
132
|
|
@@ -115,9 +135,9 @@ module Airbrake
|
|
115
135
|
with_grouped_payload(payload) do |resource_hash, destination|
|
116
136
|
url = URI.join(
|
117
137
|
@config.host,
|
118
|
-
"api/v5/projects/#{@config.project_id}/#{destination}"
|
138
|
+
"api/v5/projects/#{@config.project_id}/#{destination}",
|
119
139
|
)
|
120
|
-
|
140
|
+
sender.send(resource_hash, promise, url)
|
121
141
|
end
|
122
142
|
|
123
143
|
promise
|
@@ -152,4 +172,5 @@ module Airbrake
|
|
152
172
|
end
|
153
173
|
end
|
154
174
|
end
|
175
|
+
# rubocop:enable Metrics/ClassLength
|
155
176
|
end
|
data/lib/airbrake-ruby/query.rb
CHANGED
@@ -11,6 +11,8 @@ module Airbrake
|
|
11
11
|
include HashKeyable
|
12
12
|
include Ignorable
|
13
13
|
include Stashable
|
14
|
+
include Mergeable
|
15
|
+
include Grouppable
|
14
16
|
|
15
17
|
def initialize(
|
16
18
|
method:,
|
@@ -34,10 +36,6 @@ module Airbrake
|
|
34
36
|
'queries'
|
35
37
|
end
|
36
38
|
|
37
|
-
def groups
|
38
|
-
{}
|
39
|
-
end
|
40
|
-
|
41
39
|
def to_h
|
42
40
|
{
|
43
41
|
'method' => method,
|
@@ -46,7 +44,7 @@ module Airbrake
|
|
46
44
|
'time' => @start_time_utc,
|
47
45
|
'function' => func,
|
48
46
|
'file' => file,
|
49
|
-
'line' => line
|
47
|
+
'line' => line,
|
50
48
|
}.delete_if { |_key, val| val.nil? }
|
51
49
|
end
|
52
50
|
# rubocop:enable Metrics/ParameterLists, Metrics/BlockLength
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Airbrake
|
2
|
+
# Queue represents a queue (worker).
|
3
|
+
#
|
4
|
+
# @see Airbrake.notify_queue
|
5
|
+
# @api public
|
6
|
+
# @since v4.9.0
|
7
|
+
# rubocop:disable Metrics/BlockLength
|
8
|
+
Queue = Struct.new(:queue, :error_count, :groups, :start_time, :end_time) do
|
9
|
+
include HashKeyable
|
10
|
+
include Ignorable
|
11
|
+
include Stashable
|
12
|
+
|
13
|
+
def initialize(
|
14
|
+
queue:,
|
15
|
+
error_count:,
|
16
|
+
groups: {},
|
17
|
+
start_time: Time.now,
|
18
|
+
end_time: Time.now
|
19
|
+
)
|
20
|
+
@start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
|
21
|
+
super(queue, error_count, groups, start_time, end_time)
|
22
|
+
end
|
23
|
+
|
24
|
+
def destination
|
25
|
+
'queues-stats'
|
26
|
+
end
|
27
|
+
|
28
|
+
def cargo
|
29
|
+
'queues'
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_h
|
33
|
+
{
|
34
|
+
'queue' => queue,
|
35
|
+
'errorCount' => error_count,
|
36
|
+
'time' => @start_time_utc,
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def hash
|
41
|
+
{
|
42
|
+
'queue' => queue,
|
43
|
+
'time' => @start_time_utc,
|
44
|
+
}.hash
|
45
|
+
end
|
46
|
+
|
47
|
+
def merge(other)
|
48
|
+
self.error_count += other.error_count
|
49
|
+
end
|
50
|
+
end
|
51
|
+
# rubocop:enable Metrics/BlockLength
|
52
|
+
end
|
@@ -9,6 +9,8 @@ module Airbrake
|
|
9
9
|
include HashKeyable
|
10
10
|
include Ignorable
|
11
11
|
include Stashable
|
12
|
+
include Mergeable
|
13
|
+
include Grouppable
|
12
14
|
|
13
15
|
def initialize(
|
14
16
|
method:,
|
@@ -29,16 +31,12 @@ module Airbrake
|
|
29
31
|
'routes'
|
30
32
|
end
|
31
33
|
|
32
|
-
def groups
|
33
|
-
{}
|
34
|
-
end
|
35
|
-
|
36
34
|
def to_h
|
37
35
|
{
|
38
36
|
'method' => method,
|
39
37
|
'route' => route,
|
40
38
|
'statusCode' => status_code,
|
41
|
-
'time' => @start_time_utc
|
39
|
+
'time' => @start_time_utc,
|
42
40
|
}.delete_if { |_key, val| val.nil? }
|
43
41
|
end
|
44
42
|
end
|
data/lib/airbrake-ruby/stat.rb
CHANGED
data/spec/airbrake_spec.rb
CHANGED
@@ -51,28 +51,6 @@ RSpec.describe Airbrake do
|
|
51
51
|
expect(described_class).to be_configured
|
52
52
|
end
|
53
53
|
|
54
|
-
context "when a notifier was configured" do
|
55
|
-
before do
|
56
|
-
expect(described_class).to receive(:configured?).and_return(true)
|
57
|
-
end
|
58
|
-
|
59
|
-
it "closes previously configured notice notifier" do
|
60
|
-
expect(described_class).to receive(:close)
|
61
|
-
described_class.configure {}
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
context "when a notifier wasn't configured" do
|
66
|
-
before do
|
67
|
-
expect(described_class).to receive(:configured?).and_return(false)
|
68
|
-
end
|
69
|
-
|
70
|
-
it "doesn't close previously configured notice notifier" do
|
71
|
-
expect(described_class).not_to receive(:close)
|
72
|
-
described_class.configure {}
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
54
|
context "when called multiple times" do
|
77
55
|
it "doesn't overwrite performance notifier" do
|
78
56
|
described_class.configure {}
|
@@ -182,19 +160,6 @@ RSpec.describe Airbrake do
|
|
182
160
|
end
|
183
161
|
end
|
184
162
|
|
185
|
-
describe "#reset" do
|
186
|
-
context "when Airbrake was previously configured" do
|
187
|
-
before do
|
188
|
-
expect(described_class).to receive(:configured?).and_return(true)
|
189
|
-
end
|
190
|
-
|
191
|
-
it "closes notice notifier" do
|
192
|
-
expect(described_class).to receive(:close)
|
193
|
-
subject.reset
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
163
|
describe "#notify_request" do
|
199
164
|
context "when :stash key is not provided" do
|
200
165
|
it "doesn't add anything to the stash of the request" do
|
@@ -206,7 +171,7 @@ RSpec.describe Airbrake do
|
|
206
171
|
method: 'GET',
|
207
172
|
route: '/',
|
208
173
|
status_code: 200,
|
209
|
-
start_time: Time.now
|
174
|
+
start_time: Time.now,
|
210
175
|
)
|
211
176
|
end
|
212
177
|
end
|
@@ -222,14 +187,30 @@ RSpec.describe Airbrake do
|
|
222
187
|
method: 'GET',
|
223
188
|
route: '/',
|
224
189
|
status_code: 200,
|
225
|
-
start_time: Time.now
|
190
|
+
start_time: Time.now,
|
226
191
|
},
|
227
|
-
request_id: 1
|
192
|
+
request_id: 1,
|
228
193
|
)
|
229
194
|
end
|
230
195
|
end
|
231
196
|
end
|
232
197
|
|
198
|
+
describe "#notify_request_sync" do
|
199
|
+
it "notifies request synchronously" do
|
200
|
+
expect(described_class.performance_notifier).to receive(:notify_sync)
|
201
|
+
|
202
|
+
described_class.notify_request_sync(
|
203
|
+
{
|
204
|
+
method: 'GET',
|
205
|
+
route: '/',
|
206
|
+
status_code: 200,
|
207
|
+
start_time: Time.now,
|
208
|
+
},
|
209
|
+
request_id: 1,
|
210
|
+
)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
233
214
|
describe "#notify_query" do
|
234
215
|
context "when :stash key is not provided" do
|
235
216
|
it "doesn't add anything to the stash of the query" do
|
@@ -241,7 +222,7 @@ RSpec.describe Airbrake do
|
|
241
222
|
method: 'GET',
|
242
223
|
route: '/',
|
243
224
|
query: '',
|
244
|
-
start_time: Time.now
|
225
|
+
start_time: Time.now,
|
245
226
|
)
|
246
227
|
end
|
247
228
|
end
|
@@ -257,14 +238,30 @@ RSpec.describe Airbrake do
|
|
257
238
|
method: 'GET',
|
258
239
|
route: '/',
|
259
240
|
query: '',
|
260
|
-
start_time: Time.now
|
241
|
+
start_time: Time.now,
|
261
242
|
},
|
262
|
-
request_id: 1
|
243
|
+
request_id: 1,
|
263
244
|
)
|
264
245
|
end
|
265
246
|
end
|
266
247
|
end
|
267
248
|
|
249
|
+
describe "#notify_query_sync" do
|
250
|
+
it "notifies query synchronously" do
|
251
|
+
expect(described_class.performance_notifier).to receive(:notify_sync)
|
252
|
+
|
253
|
+
described_class.notify_query_sync(
|
254
|
+
{
|
255
|
+
method: 'GET',
|
256
|
+
route: '/',
|
257
|
+
query: '',
|
258
|
+
start_time: Time.now,
|
259
|
+
},
|
260
|
+
request_id: 1,
|
261
|
+
)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
268
265
|
describe "#notify_performance_breakdown" do
|
269
266
|
context "when :stash key is not provided" do
|
270
267
|
it "doesn't add anything to the stash of the performance breakdown" do
|
@@ -276,7 +273,7 @@ RSpec.describe Airbrake do
|
|
276
273
|
method: 'GET',
|
277
274
|
route: '/',
|
278
275
|
query: '',
|
279
|
-
start_time: Time.now
|
276
|
+
start_time: Time.now,
|
280
277
|
)
|
281
278
|
end
|
282
279
|
end
|
@@ -284,7 +281,7 @@ RSpec.describe Airbrake do
|
|
284
281
|
context "when :stash key is provided" do
|
285
282
|
it "adds the value as the stash of the performance breakdown" do
|
286
283
|
expect(
|
287
|
-
described_class.performance_notifier
|
284
|
+
described_class.performance_notifier,
|
288
285
|
).to receive(:notify) do |performance_breakdown|
|
289
286
|
expect(performance_breakdown.stash).to eq(request_id: 1)
|
290
287
|
end
|
@@ -295,14 +292,76 @@ RSpec.describe Airbrake do
|
|
295
292
|
route: '/',
|
296
293
|
response_type: :html,
|
297
294
|
groups: {},
|
298
|
-
start_time: Time.now
|
295
|
+
start_time: Time.now,
|
296
|
+
},
|
297
|
+
request_id: 1,
|
298
|
+
)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
describe "#notify_performance_breakdown_sync" do
|
304
|
+
it "notifies performance breakdown synchronously" do
|
305
|
+
expect(described_class.performance_notifier).to receive(:notify_sync)
|
306
|
+
|
307
|
+
described_class.notify_performance_breakdown_sync(
|
308
|
+
{
|
309
|
+
method: 'GET',
|
310
|
+
route: '/',
|
311
|
+
response_type: :html,
|
312
|
+
groups: {},
|
313
|
+
start_time: Time.now,
|
314
|
+
},
|
315
|
+
request_id: 1,
|
316
|
+
)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
describe "#notify_queue" do
|
321
|
+
context "when :stash key is not provided" do
|
322
|
+
it "doesn't add anything to the stash of the queue" do
|
323
|
+
expect(described_class.performance_notifier).to receive(:notify) do |queue|
|
324
|
+
expect(queue.stash).to be_empty
|
325
|
+
end
|
326
|
+
|
327
|
+
described_class.notify_queue(
|
328
|
+
queue: 'bananas',
|
329
|
+
error_count: 10,
|
330
|
+
)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
context "when :stash key is provided" do
|
335
|
+
it "adds the value as the stash of the queue" do
|
336
|
+
expect(described_class.performance_notifier).to receive(:notify) do |queue|
|
337
|
+
expect(queue.stash).to eq(request_id: 1)
|
338
|
+
end
|
339
|
+
|
340
|
+
described_class.notify_queue(
|
341
|
+
{
|
342
|
+
queue: 'bananas',
|
343
|
+
error_count: 10,
|
299
344
|
},
|
300
|
-
request_id: 1
|
345
|
+
request_id: 1,
|
301
346
|
)
|
302
347
|
end
|
303
348
|
end
|
304
349
|
end
|
305
350
|
|
351
|
+
describe "#notify_queue_sync" do
|
352
|
+
it "notifies queue synchronously" do
|
353
|
+
expect(described_class.performance_notifier).to receive(:notify_sync)
|
354
|
+
|
355
|
+
described_class.notify_queue_sync(
|
356
|
+
{
|
357
|
+
queue: 'bananas',
|
358
|
+
error_count: 10,
|
359
|
+
},
|
360
|
+
request_id: 1,
|
361
|
+
)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
306
365
|
describe ".performance_notifier" do
|
307
366
|
it "returns a performance notifier" do
|
308
367
|
expect(described_class.performance_notifier)
|
@@ -321,4 +380,35 @@ RSpec.describe Airbrake do
|
|
321
380
|
expect(described_class.deploy_notifier).to be_an(Airbrake::DeployNotifier)
|
322
381
|
end
|
323
382
|
end
|
383
|
+
|
384
|
+
describe ".close" do
|
385
|
+
after { Airbrake.reset }
|
386
|
+
|
387
|
+
context "when notice_notifier is defined" do
|
388
|
+
it "gets closed" do
|
389
|
+
expect(Airbrake.notice_notifier).to receive(:close)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
context "when notice_notifier is undefined" do
|
394
|
+
it "doesn't get closed (because it wasn't initialized)" do
|
395
|
+
Airbrake.instance_variable_set(:@notice_notifier, nil)
|
396
|
+
expect_any_instance_of(Airbrake::NoticeNotifier).not_to receive(:close)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
context "when performance_notifier is defined" do
|
401
|
+
it "gets closed" do
|
402
|
+
expect(Airbrake.performance_notifier).to receive(:close)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
context "when perforance_notifier is undefined" do
|
407
|
+
it "doesn't get closed (because it wasn't initialized)" do
|
408
|
+
Airbrake.instance_variable_set(:@performance_notifier, nil)
|
409
|
+
expect_any_instance_of(Airbrake::PerformanceNotifier)
|
410
|
+
.not_to receive(:close)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
324
414
|
end
|