exception_handling 2.17.0.pre.tstarck.1 → 3.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -3
  3. data/.ruby-version +1 -1
  4. data/Gemfile +16 -12
  5. data/Gemfile.lock +138 -153
  6. data/README.md +21 -90
  7. data/Rakefile +11 -8
  8. data/exception_handling.gemspec +10 -14
  9. data/lib/exception_handling/exception_info.rb +11 -15
  10. data/lib/exception_handling/honeybadger_callbacks.rb +59 -0
  11. data/lib/exception_handling/log_stub_error.rb +1 -2
  12. data/lib/exception_handling/methods.rb +53 -6
  13. data/lib/exception_handling/testing.rb +10 -20
  14. data/lib/exception_handling/version.rb +1 -1
  15. data/lib/exception_handling.rb +34 -135
  16. data/semaphore_ci/setup.sh +3 -0
  17. data/{spec → test}/helpers/exception_helpers.rb +2 -2
  18. data/{spec/spec_helper.rb → test/test_helper.rb} +45 -75
  19. data/test/unit/exception_handling/exception_catalog_test.rb +85 -0
  20. data/test/unit/exception_handling/exception_description_test.rb +82 -0
  21. data/{spec/unit/exception_handling/exception_info_spec.rb → test/unit/exception_handling/exception_info_test.rb} +114 -170
  22. data/test/unit/exception_handling/honeybadger_callbacks_test.rb +122 -0
  23. data/{spec/unit/exception_handling/log_error_stub_spec.rb → test/unit/exception_handling/log_error_stub_test.rb} +22 -38
  24. data/{spec/unit/exception_handling/mailer_spec.rb → test/unit/exception_handling/mailer_test.rb} +18 -17
  25. data/test/unit/exception_handling/methods_test.rb +84 -0
  26. data/test/unit/exception_handling/sensu_test.rb +52 -0
  27. data/test/unit/exception_handling_test.rb +1109 -0
  28. metadata +59 -99
  29. data/.github/CODEOWNERS +0 -1
  30. data/.github/workflows/pipeline.yml +0 -36
  31. data/.rspec +0 -3
  32. data/.tool-versions +0 -1
  33. data/Appraisals +0 -19
  34. data/CHANGELOG.md +0 -149
  35. data/gemfiles/rails_5.gemfile +0 -18
  36. data/gemfiles/rails_6.gemfile +0 -18
  37. data/gemfiles/rails_7.gemfile +0 -18
  38. data/lib/exception_handling/escalate_callback.rb +0 -19
  39. data/lib/exception_handling/logging_methods.rb +0 -27
  40. data/spec/rake_test_warning_false.rb +0 -20
  41. data/spec/unit/exception_handling/escalate_callback_spec.rb +0 -81
  42. data/spec/unit/exception_handling/exception_catalog_spec.rb +0 -85
  43. data/spec/unit/exception_handling/exception_description_spec.rb +0 -82
  44. data/spec/unit/exception_handling/logging_methods_spec.rb +0 -38
  45. data/spec/unit/exception_handling/methods_spec.rb +0 -105
  46. data/spec/unit/exception_handling/sensu_spec.rb +0 -51
  47. data/spec/unit/exception_handling_spec.rb +0 -1465
  48. /data/{spec → test}/helpers/controller_helpers.rb +0 -0
@@ -0,0 +1,1109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('../test_helper', __dir__)
4
+ require_test_helper 'controller_helpers'
5
+ require_test_helper 'exception_helpers'
6
+
7
+ class ExceptionHandlingTest < ActiveSupport::TestCase
8
+ include ControllerHelpers
9
+ include ExceptionHelpers
10
+
11
+ def dont_stub_log_error
12
+ true
13
+ end
14
+
15
+ def append_organization_info_config(data)
16
+ data[:user_details] = {}
17
+ data[:user_details][:username] = "CaryP"
18
+ data[:user_details][:organization] = "Invoca Engineering Dept."
19
+ rescue StandardError
20
+ # don't let these out!
21
+ end
22
+
23
+ def custom_data_callback_returns_nil_message_exception(_data)
24
+ raise_exception_with_nil_message
25
+ end
26
+
27
+ def log_error_callback(_data, _ex, _treat_like_warning, _honeybadger_status)
28
+ @fail_count += 1
29
+ end
30
+
31
+ def log_error_callback_config(data, _ex, treat_like_warning, honeybadger_status)
32
+ @callback_data = data
33
+ @treat_like_warning = treat_like_warning
34
+ @fail_count += 1
35
+ @honeybadger_status = honeybadger_status
36
+ end
37
+
38
+ def log_error_callback_with_failure(_data, _ex)
39
+ raise "this should be rescued"
40
+ end
41
+
42
+ def log_error_callback_returns_nil_message_exception(_data, _ex)
43
+ raise_exception_with_nil_message
44
+ end
45
+
46
+ module EventMachineStub
47
+ class << self
48
+ attr_accessor :block
49
+
50
+ def schedule(&block)
51
+ @block = block
52
+ end
53
+ end
54
+ end
55
+
56
+ class DNSResolvStub
57
+ class << self
58
+ attr_accessor :callback_block
59
+ attr_accessor :errback_block
60
+
61
+ def resolve(_hostname)
62
+ self
63
+ end
64
+
65
+ def callback(&block)
66
+ @callback_block = block
67
+ end
68
+
69
+ def errback(&block)
70
+ @errback_block = block
71
+ end
72
+ end
73
+ end
74
+
75
+ class SmtpClientStub
76
+ class << self
77
+ attr_reader :block
78
+ attr_reader :last_method
79
+
80
+ def errback(&block)
81
+ @block = block
82
+ end
83
+
84
+ def send_hash
85
+ @send_hash ||= {}
86
+ end
87
+
88
+ def send(hash)
89
+ @last_method = :send
90
+ send_hash.clear
91
+ send_hash.merge!(hash)
92
+ self
93
+ end
94
+
95
+ def asend(hash)
96
+ send(hash)
97
+ @last_method = :asend
98
+ self
99
+ end
100
+ end
101
+ end
102
+
103
+ class SmtpClientErrbackStub < SmtpClientStub
104
+ end
105
+
106
+ context "with warn and honeybadger notify stubbed" do
107
+ setup do
108
+ stub(ExceptionHandling).warn(anything)
109
+ stub(Honeybadger).notify(anything)
110
+ end
111
+
112
+ context "#log_error" do
113
+ should "take in additional keyword args as logging context and pass them to the logger (using preferrred log_context:)" do
114
+ ExceptionHandling.log_error('This is an Error', 'This is the prefix context', service_name: 'exception_handling')
115
+ assert_match(/This is an Error/, logged_excluding_reload_filter.last[:message])
116
+ assert_not_empty logged_excluding_reload_filter.last[:context]
117
+ assert_equal({ service_name: 'exception_handling' }, logged_excluding_reload_filter.last[:context])
118
+ end
119
+
120
+ should "take in additional keyword args as logging context and pass them to the logger (using **)" do
121
+ ExceptionHandling.log_error('This is an Error', 'This is the prefix context', service_name: 'exception_handling')
122
+ assert_match(/This is an Error/, logged_excluding_reload_filter.last[:message])
123
+ assert_not_empty logged_excluding_reload_filter.last[:context]
124
+ assert_equal logged_excluding_reload_filter.last[:context], service_name: 'exception_handling'
125
+ end
126
+ end
127
+
128
+ context "#log_warning" do
129
+ should "have empty array as a backtrace" do
130
+ mock(ExceptionHandling).log_error(is_a(ExceptionHandling::Warning), anything) do |error|
131
+ assert_equal [], error.backtrace
132
+ end
133
+ ExceptionHandling.log_warning('Now with empty array as a backtrace!')
134
+ end
135
+
136
+ should "take in additional key word args as logging context and pass them to the logger" do
137
+ ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
138
+ assert_match(/This is a Warning/, logged_excluding_reload_filter.last[:message])
139
+ assert_not_empty logged_excluding_reload_filter.last[:context]
140
+ assert_equal logged_excluding_reload_filter.last[:context], service_name: 'exception_handling'
141
+ end
142
+ end
143
+
144
+ context "#log_info" do
145
+ should "take in additional key word args as logging context and pass them to the logger" do
146
+ ExceptionHandling.log_warning('This is an Info', service_name: 'exception_handling')
147
+ assert_match(/This is an Info/, logged_excluding_reload_filter.last[:message])
148
+ assert_not_empty logged_excluding_reload_filter.last[:context]
149
+ assert_equal logged_excluding_reload_filter.last[:context], service_name: 'exception_handling'
150
+ end
151
+ end
152
+
153
+ context "#log_debug" do
154
+ should "take in additional key word args as logging context and pass them to the logger" do
155
+ ExceptionHandling.log_warning('This is a Debug', service_name: 'exception_handling')
156
+ assert_match(/This is a Debug/, logged_excluding_reload_filter.last[:message])
157
+ assert_not_empty logged_excluding_reload_filter.last[:context]
158
+ assert_equal logged_excluding_reload_filter.last[:context], service_name: 'exception_handling'
159
+ end
160
+ end
161
+
162
+ context "configuration with custom_data_hook or post_log_error_hook" do
163
+ teardown do
164
+ ExceptionHandling.custom_data_hook = nil
165
+ ExceptionHandling.post_log_error_hook = nil
166
+ end
167
+
168
+ should "support a custom_data_hook" do
169
+ capture_notifications
170
+
171
+ ExceptionHandling.custom_data_hook = method(:append_organization_info_config)
172
+ ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
173
+
174
+ assert_match(/Invoca Engineering Dept./, sent_notifications.last.enhanced_data['user_details'].to_s)
175
+ end
176
+
177
+ should "support a log_error hook, and pass exception_data, treat_like_warning, and logged_to_honeybadger to it" do
178
+ @fail_count = 0
179
+ @honeybadger_status = nil
180
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
181
+
182
+ notify_args = []
183
+ mock(Honeybadger).notify.with_any_args { |info| notify_args << info; '06220c5a-b471-41e5-baeb-de247da45a56' }
184
+ ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
185
+ assert_equal 1, @fail_count
186
+ assert_equal false, @treat_like_warning
187
+ assert_equal :success, @honeybadger_status
188
+
189
+ assert_equal "this is used by a test", @callback_data["notes"]
190
+ assert_equal 1, notify_args.size, notify_args.inspect
191
+ assert_match(/this is used by a test/, notify_args.last[:context].to_s)
192
+ end
193
+
194
+ should "plumb treat_like_warning and logged_to_honeybadger to log error hook" do
195
+ @fail_count = 0
196
+ @honeybadger_status = nil
197
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
198
+ ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true)
199
+ assert_equal 1, @fail_count
200
+ assert_equal true, @treat_like_warning
201
+ assert_equal :skipped, @honeybadger_status
202
+ end
203
+
204
+ should "include logging context in the exception data" do
205
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
206
+ ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true, log_context_test: "contextual_logging")
207
+
208
+ expected_log_context = {
209
+ "log_context_test" => "contextual_logging"
210
+ }
211
+ assert_equal expected_log_context, @callback_data[:log_context]
212
+ end
213
+
214
+ should "support rescue exceptions from a log_error hook" do
215
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_with_failure)
216
+ log_info_messages = []
217
+ stub(ExceptionHandling.logger).info.with_any_args do |message, _|
218
+ log_info_messages << message
219
+ end
220
+ assert_nothing_raised { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }
221
+ assert log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }
222
+ end
223
+
224
+ should "handle nil message exceptions resulting from the log_error hook" do
225
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_returns_nil_message_exception)
226
+ log_info_messages = []
227
+ stub(ExceptionHandling.logger).info.with_any_args do |message, _|
228
+ log_info_messages << message
229
+ end
230
+ assert_nothing_raised { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }
231
+ assert log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }
232
+ end
233
+
234
+ should "handle nil message exceptions resulting from the custom data hook" do
235
+ ExceptionHandling.custom_data_hook = method(:custom_data_callback_returns_nil_message_exception)
236
+ log_info_messages = []
237
+ stub(ExceptionHandling.logger).info.with_any_args do |message, _|
238
+ log_info_messages << message
239
+ end
240
+ assert_nothing_raised { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }
241
+ assert log_info_messages.find { |message| message =~ /Unable to execute custom custom_data_hook callback/ }
242
+ end
243
+ end
244
+
245
+ context "Exception Handling" do
246
+ context "default_metric_name" do
247
+ context "when metric_name is present in exception_data" do
248
+ should "include metric_name in resulting metric name" do
249
+ exception = StandardError.new('this is an exception')
250
+ metric = ExceptionHandling.default_metric_name({ 'metric_name' => 'special_metric' }, exception, true)
251
+ assert_equal 'exception_handling.special_metric', metric
252
+ end
253
+ end
254
+
255
+ context "when metric_name is not present in exception_data" do
256
+ should "return exception_handling.warning when using log warning" do
257
+ warning = ExceptionHandling::Warning.new('this is a warning')
258
+ metric = ExceptionHandling.default_metric_name({}, warning, false)
259
+ assert_equal 'exception_handling.warning', metric
260
+ end
261
+
262
+ should "return exception_handling.exception when using log error" do
263
+ exception = StandardError.new('this is an exception')
264
+ metric = ExceptionHandling.default_metric_name({}, exception, false)
265
+ assert_equal 'exception_handling.exception', metric
266
+ end
267
+
268
+ context "when using log error with treat_like_warning" do
269
+ should "return exception_handling.unforwarded_exception when exception not present" do
270
+ metric = ExceptionHandling.default_metric_name({}, nil, true)
271
+ assert_equal 'exception_handling.unforwarded_exception', metric
272
+ end
273
+
274
+ should "return exception_handling.unforwarded_exception with exception classname when exception is present" do
275
+ module SomeModule
276
+ class SomeException < StandardError
277
+ end
278
+ end
279
+
280
+ exception = SomeModule::SomeException.new('this is an exception')
281
+ metric = ExceptionHandling.default_metric_name({}, exception, true)
282
+ assert_equal 'exception_handling.unforwarded_exception_SomeException', metric
283
+ end
284
+ end
285
+ end
286
+ end
287
+
288
+ context "default_honeybadger_metric_name" do
289
+ should "return exception_handling.honeybadger.success when status is :success" do
290
+ metric = ExceptionHandling.default_honeybadger_metric_name(:success)
291
+ assert_equal 'exception_handling.honeybadger.success', metric
292
+ end
293
+
294
+ should "return exception_handling.honeybadger.failure when status is :failure" do
295
+ metric = ExceptionHandling.default_honeybadger_metric_name(:failure)
296
+ assert_equal 'exception_handling.honeybadger.failure', metric
297
+ end
298
+
299
+ should "return exception_handling.honeybadger.skipped when status is :skipped" do
300
+ metric = ExceptionHandling.default_honeybadger_metric_name(:skipped)
301
+ assert_equal 'exception_handling.honeybadger.skipped', metric
302
+ end
303
+
304
+ should "return exception_handling.honeybadger.unknown_status when status is not recognized" do
305
+ metric = ExceptionHandling.default_honeybadger_metric_name(nil)
306
+ assert_equal 'exception_handling.honeybadger.unknown_status', metric
307
+ end
308
+ end
309
+
310
+ context "ExceptionHandling.ensure_safe" do
311
+ should "log an exception with call stack if an exception is raised." do
312
+ mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
313
+ ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
314
+ end
315
+
316
+ should "log an exception with call stack if an ActionView template exception is raised." do
317
+ mock(ExceptionHandling.logger).fatal(/\(Error:\d+\) ActionView::Template::Error \(blah\):\n /, anything)
318
+ ExceptionHandling.ensure_safe { raise ActionView::TemplateError.new({}, ArgumentError.new("blah")) }
319
+ end
320
+
321
+ should "should not log an exception if an exception is not raised." do
322
+ dont_allow(ExceptionHandling.logger).fatal
323
+ ExceptionHandling.ensure_safe { ; }
324
+ end
325
+
326
+ should "return its value if used during an assignment" do
327
+ dont_allow(ExceptionHandling.logger).fatal
328
+ b = ExceptionHandling.ensure_safe { 5 }
329
+ assert_equal 5, b
330
+ end
331
+
332
+ should "return nil if an exception is raised during an assignment" do
333
+ mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
334
+ b = ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
335
+ assert_nil b
336
+ end
337
+
338
+ should "allow a message to be appended to the error when logged." do
339
+ mock(ExceptionHandling.logger).fatal(/mooo \(blah\):\n.*exception_handling_test\.rb/, anything)
340
+ b = ExceptionHandling.ensure_safe("mooo") { raise ArgumentError, "blah" }
341
+ assert_nil b
342
+ end
343
+
344
+ should "only rescue StandardError and descendents" do
345
+ assert_raise(Exception) { ExceptionHandling.ensure_safe("mooo") { raise Exception } }
346
+
347
+ mock(ExceptionHandling.logger).fatal(/mooo \(blah\):\n.*exception_handling_test\.rb/, anything)
348
+
349
+ b = ExceptionHandling.ensure_safe("mooo") { raise StandardError, "blah" }
350
+ assert_nil b
351
+ end
352
+ end
353
+
354
+ context "ExceptionHandling.ensure_completely_safe" do
355
+ should "log an exception if an exception is raised." do
356
+ mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
357
+ ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
358
+ end
359
+
360
+ should "should not log an exception if an exception is not raised." do
361
+ mock(ExceptionHandling.logger).fatal.times(0)
362
+ ExceptionHandling.ensure_completely_safe { ; }
363
+ end
364
+
365
+ should "return its value if used during an assignment" do
366
+ mock(ExceptionHandling.logger).fatal.times(0)
367
+ b = ExceptionHandling.ensure_completely_safe { 5 }
368
+ assert_equal 5, b
369
+ end
370
+
371
+ should "return nil if an exception is raised during an assignment" do
372
+ mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything) { nil }
373
+ b = ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
374
+ assert_nil b
375
+ end
376
+
377
+ should "allow a message to be appended to the error when logged." do
378
+ mock(ExceptionHandling.logger).fatal(/mooo \(blah\):\n.*exception_handling_test\.rb/, anything)
379
+ b = ExceptionHandling.ensure_completely_safe("mooo") { raise ArgumentError, "blah" }
380
+ assert_nil b
381
+ end
382
+
383
+ should "rescue any instance or child of Exception" do
384
+ mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
385
+ ExceptionHandling.ensure_completely_safe { raise Exception, "blah" }
386
+ end
387
+
388
+ should "not rescue the special exceptions that Ruby uses" do
389
+ [SystemExit, SystemStackError, NoMemoryError, SecurityError].each do |exception|
390
+ assert_raise exception do
391
+ ExceptionHandling.ensure_completely_safe do
392
+ raise exception
393
+ end
394
+ end
395
+ end
396
+ end
397
+ end
398
+
399
+ context "ExceptionHandling.ensure_escalation" do
400
+ setup do
401
+ capture_notifications
402
+ ActionMailer::Base.deliveries.clear
403
+ end
404
+
405
+ should "log the exception as usual and send the proper email" do
406
+ mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
407
+ ExceptionHandling.ensure_escalation("Favorite Feature") { raise ArgumentError, "blah" }
408
+ assert_equal 1, ActionMailer::Base.deliveries.count
409
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
410
+
411
+ email = ActionMailer::Base.deliveries.last
412
+ assert_equal "#{ExceptionHandling.email_environment} Escalation: Favorite Feature", email.subject
413
+ assert_match 'ArgumentError: blah', email.body.to_s
414
+ assert_match ExceptionHandling.last_exception_timestamp.to_s, email.body.to_s
415
+ end
416
+
417
+ should "should not escalate if an exception is not raised." do
418
+ dont_allow(ExceptionHandling.logger).fatal
419
+ ExceptionHandling.ensure_escalation('Ignored') { ; }
420
+ assert_equal 0, ActionMailer::Base.deliveries.count
421
+ end
422
+
423
+ should "log if the escalation email cannot be sent" do
424
+ any_instance_of(Mail::Message) do |message|
425
+ mock(message).deliver { raise RuntimeError.new, "Delivery Error" }
426
+ end
427
+ log_fatals = []
428
+ stub(ExceptionHandling.logger) do |logger|
429
+ logger.fatal.with_any_args { |*args| log_fatals << args }
430
+ end
431
+
432
+ ExceptionHandling.ensure_escalation("ensure context") { raise ArgumentError, "first_test_exception" }
433
+
434
+ assert_match /ArgumentError.*first_test_exception/, log_fatals[0].first
435
+ assert_match /safe_email_deliver.*Delivery Error/, log_fatals[1].first
436
+
437
+ assert_equal 2, log_fatals.size, log_fatals.inspect
438
+
439
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect # still sent to honeybadger
440
+ end
441
+
442
+ should "allow the caller to specify custom recipients" do
443
+ custom_recipients = ['something@invoca.com']
444
+ mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
445
+ ExceptionHandling.ensure_escalation("Favorite Feature", custom_recipients) { raise ArgumentError, "blah" }
446
+ assert_equal 1, ActionMailer::Base.deliveries.count
447
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
448
+
449
+ email = ActionMailer::Base.deliveries.last
450
+ assert_equal "#{ExceptionHandling.email_environment} Escalation: Favorite Feature", email.subject
451
+ assert_match 'ArgumentError: blah', email.body.to_s
452
+ assert_match ExceptionHandling.last_exception_timestamp.to_s, email.body.to_s
453
+ assert_equal custom_recipients, email.to
454
+ end
455
+ end
456
+
457
+ context "ExceptionHandling.ensure_alert" do
458
+ should "log the exception as usual and fire a sensu event" do
459
+ mock(ExceptionHandling::Sensu).generate_event("Favorite Feature", "test context\nblah")
460
+ mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
461
+ ExceptionHandling.ensure_alert('Favorite Feature', 'test context') { raise ArgumentError, "blah" }
462
+ end
463
+
464
+ should "should not send sensu event if an exception is not raised." do
465
+ dont_allow(ExceptionHandling.logger).fatal
466
+ dont_allow(ExceptionHandling::Sensu).generate_event
467
+ ExceptionHandling.ensure_alert('Ignored', 'test context') { ; }
468
+ end
469
+
470
+ should "log if the sensu event could not be sent" do
471
+ mock(ExceptionHandling::Sensu).send_event(anything) { raise "Failed to send" }
472
+ mock(ExceptionHandling.logger) do |logger|
473
+ logger.fatal(/first_test_exception/, anything)
474
+ logger.fatal(/Failed to send/, anything)
475
+ end
476
+ ExceptionHandling.ensure_alert("Not Used", 'test context') { raise ArgumentError, "first_test_exception" }
477
+ end
478
+
479
+ should "log if the exception message is nil" do
480
+ mock(ExceptionHandling::Sensu).generate_event("some alert", "test context\n")
481
+ ExceptionHandling.ensure_alert('some alert', 'test context') { raise_exception_with_nil_message }
482
+ end
483
+ end
484
+
485
+ context "ExceptionHandling.escalate_to_production_support" do
486
+ should "notify production support" do
487
+ subject = "Runtime Error found!"
488
+ exception = RuntimeError.new("Test")
489
+ recipients = ["prodsupport@example.com"]
490
+
491
+ mock(ExceptionHandling).production_support_recipients { recipients }.times(2)
492
+ mock(ExceptionHandling).escalate(subject, exception, ExceptionHandling.last_exception_timestamp, recipients)
493
+ ExceptionHandling.escalate_to_production_support(exception, subject)
494
+ end
495
+ end
496
+
497
+ context "exception timestamp" do
498
+ setup do
499
+ Time.now_override = Time.parse('1986-5-21 4:17 am UTC')
500
+ end
501
+
502
+ should "include the timestamp when the exception is logged" do
503
+ capture_notifications
504
+
505
+ mock(ExceptionHandling.logger).fatal(/\(Error:517033020\) ArgumentError context \(blah\):\n.*exception_handling_test\.rb/, anything)
506
+ b = ExceptionHandling.ensure_safe("context") { raise ArgumentError, "blah" }
507
+ assert_nil b
508
+
509
+ assert_equal 517_033_020, ExceptionHandling.last_exception_timestamp
510
+
511
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
512
+
513
+ assert_equal 517_033_020, sent_notifications.last.enhanced_data['timestamp']
514
+ end
515
+ end
516
+
517
+ should "log the error if the exception message is nil" do
518
+ capture_notifications
519
+
520
+ ExceptionHandling.log_error(exception_with_nil_message)
521
+
522
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
523
+ assert_equal 'RuntimeError: ', sent_notifications.last.enhanced_data['error_string']
524
+ end
525
+
526
+ should "log the error if the exception message is nil and the exception context is a hash" do
527
+ capture_notifications
528
+
529
+ ExceptionHandling.log_error(exception_with_nil_message, "SERVER_NAME" => "exceptional.com")
530
+
531
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
532
+ assert_equal 'RuntimeError: ', sent_notifications.last.enhanced_data['error_string']
533
+ end
534
+
535
+ context "Honeybadger integration" do
536
+ context "with Honeybadger not defined" do
537
+ setup do
538
+ stub(ExceptionHandling).honeybadger_defined? { false }
539
+ end
540
+
541
+ should "not invoke send_exception_to_honeybadger when log_error is executed" do
542
+ dont_allow(ExceptionHandling).send_exception_to_honeybadger
543
+ ExceptionHandling.log_error(exception_1)
544
+ end
545
+
546
+ should "not invoke send_exception_to_honeybadger when ensure_safe is executed" do
547
+ dont_allow(ExceptionHandling).send_exception_to_honeybadger
548
+ ExceptionHandling.ensure_safe { raise exception_1 }
549
+ end
550
+ end
551
+
552
+ context "with Honeybadger defined" do
553
+ teardown do
554
+ ExceptionHandling.current_controller = nil
555
+ end
556
+
557
+ should "not send_exception_to_honeybadger when log_warning is executed" do
558
+ dont_allow(ExceptionHandling).send_exception_to_honeybadger
559
+ ExceptionHandling.log_warning("This should not go to honeybadger")
560
+ end
561
+
562
+ should "not send_exception_to_honeybadger when log_error is called with a Warning" do
563
+ dont_allow(ExceptionHandling).send_exception_to_honeybadger
564
+ ExceptionHandling.log_error(ExceptionHandling::Warning.new("This should not go to honeybadger"))
565
+ end
566
+
567
+ should "invoke send_exception_to_honeybadger when log_error is executed" do
568
+ mock.proxy(ExceptionHandling).send_exception_to_honeybadger.with_any_args
569
+ ExceptionHandling.log_error(exception_1)
570
+ end
571
+
572
+ should "invoke send_exception_to_honeybadger when log_error_rack is executed" do
573
+ mock.proxy(ExceptionHandling).send_exception_to_honeybadger.with_any_args
574
+ ExceptionHandling.log_error_rack(exception_1, {}, nil)
575
+ end
576
+
577
+ should "invoke send_exception_to_honeybadger when ensure_safe is executed" do
578
+ mock.proxy(ExceptionHandling).send_exception_to_honeybadger.with_any_args
579
+ ExceptionHandling.ensure_safe { raise exception_1 }
580
+ end
581
+
582
+ should "specify error message as an empty string when notifying honeybadger if exception message is nil" do
583
+ mock(Honeybadger).notify.with_any_args do |args|
584
+ assert_equal "", args[:error_message]
585
+ end
586
+ ExceptionHandling.log_error(exception_with_nil_message)
587
+ end
588
+
589
+ should "send error details and relevant context data to Honeybadger" do
590
+ Time.now_override = Time.now
591
+ env = { server: "fe98" }
592
+ parameters = { advertiser_id: 435, controller: "some_controller" }
593
+ session = { username: "jsmith" }
594
+ request_uri = "host/path"
595
+ ExceptionHandling.current_controller = create_dummy_controller(env, parameters, session, request_uri)
596
+ stub(ExceptionHandling).server_name { "invoca_fe98" }
597
+
598
+ exception = StandardError.new("Some Exception")
599
+ exception.set_backtrace([
600
+ "test/unit/exception_handling_test.rb:847:in `exception_1'",
601
+ "test/unit/exception_handling_test.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
602
+ ])
603
+ exception_context = { "SERVER_NAME" => "exceptional.com" }
604
+
605
+ honeybadger_data = nil
606
+ mock(Honeybadger).notify.with_any_args do |data|
607
+ honeybadger_data = data
608
+ end
609
+ ExceptionHandling.log_error(exception, exception_context) do |data|
610
+ data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
611
+ data[:user_details] = { username: "jsmith" }
612
+ data[:event_response] = "Event successfully received"
613
+ data[:other_section] = "This should not be included in the response"
614
+ end
615
+
616
+ expected_data = {
617
+ error_class: :"Test Exception",
618
+ error_message: "Some Exception",
619
+ controller: "some_controller",
620
+ exception: exception,
621
+ context: {
622
+ timestamp: Time.now.to_i,
623
+ error_class: "StandardError",
624
+ server: "invoca_fe98",
625
+ exception_context: { "SERVER_NAME" => "exceptional.com" },
626
+ scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
627
+ notes: "this is used by a test",
628
+ user_details: { "username" => "jsmith" },
629
+ request: {
630
+ "params" => { "advertiser_id" => 435, "controller" => "some_controller" },
631
+ "rails_root" => "Rails.root not defined. Is this a test environment?",
632
+ "url" => "host/path"
633
+ },
634
+ session: {
635
+ "key" => nil,
636
+ "data" => { "username" => "jsmith" }
637
+ },
638
+ environment: {
639
+ "SERVER_NAME" => "exceptional.com"
640
+ },
641
+ backtrace: [
642
+ "test/unit/exception_handling_test.rb:847:in `exception_1'",
643
+ "test/unit/exception_handling_test.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
644
+ ],
645
+ event_response: "Event successfully received"
646
+ }
647
+ }
648
+ assert_equal_with_diff expected_data, honeybadger_data
649
+ end
650
+
651
+ context "with post_log_error_hook set" do
652
+ teardown do
653
+ ExceptionHandling.post_log_error_hook = nil
654
+ end
655
+
656
+ should "not send notification to honeybadger when exception description has the flag turned off and call log error callback with logged_to_honeybadger set to nil" do
657
+ @fail_count = 0
658
+ @honeybadger_status = nil
659
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
660
+ filter_list = {
661
+ NoHoneybadger: {
662
+ error: "suppress Honeybadger notification",
663
+ send_to_honeybadger: false
664
+ }
665
+ }
666
+ stub(File).mtime { incrementing_mtime }
667
+ mock(YAML).load_file.with_any_args { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }.at_least(1)
668
+
669
+ mock.proxy(ExceptionHandling).send_exception_to_honeybadger_unless_filtered.with_any_args.once
670
+ dont_allow(Honeybadger).notify
671
+ ExceptionHandling.log_error(StandardError.new("suppress Honeybadger notification"))
672
+ assert_equal :skipped, @honeybadger_status
673
+ end
674
+
675
+ should "call log error callback with logged_to_honeybadger set to false if an error occurs while attempting to notify honeybadger" do
676
+ @fail_count = 0
677
+ @honeybadger_status = nil
678
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
679
+ mock(Honeybadger).notify.with_any_args { raise "Honeybadger Notification Failure" }
680
+ ExceptionHandling.log_error(exception_1)
681
+ assert_equal :failure, @honeybadger_status
682
+ end
683
+
684
+ should "call log error callback with logged_to_honeybadger set to false on unsuccessful honeybadger notification" do
685
+ @fail_count = 0
686
+ @honeybadger_status = nil
687
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
688
+ mock(Honeybadger).notify.with_any_args { false }
689
+ ExceptionHandling.log_error(exception_1)
690
+ assert_equal :failure, @honeybadger_status
691
+ end
692
+
693
+ should "call log error callback with logged_to_honeybadger set to true on successful honeybadger notification" do
694
+ @fail_count = 0
695
+ @honeybadger_status = nil
696
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
697
+ mock(Honeybadger).notify.with_any_args { '06220c5a-b471-41e5-baeb-de247da45a56' }
698
+ ExceptionHandling.log_error(exception_1)
699
+ assert_equal :success, @honeybadger_status
700
+ end
701
+ end
702
+ end
703
+ end
704
+
705
+ class EventResponse
706
+ def to_s
707
+ "message from to_s!"
708
+ end
709
+ end
710
+
711
+ should "allow sections to have data with just a to_s method" do
712
+ capture_notifications
713
+
714
+ ExceptionHandling.log_error("This is my RingSwitch example.") do |data|
715
+ data.merge!(event_response: EventResponse.new)
716
+ end
717
+
718
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
719
+ assert_match(/message from to_s!/, sent_notifications.last.enhanced_data['event_response'].to_s)
720
+ end
721
+ end
722
+
723
+ should "return the error ID (timestamp)" do
724
+ result = ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
725
+ assert_equal ExceptionHandling.last_exception_timestamp, result
726
+ end
727
+
728
+ should "rescue exceptions that happen in log_error" do
729
+ stub(ExceptionHandling).make_exception { raise ArgumentError, "Bad argument" }
730
+ mock(ExceptionHandling).write_exception_to_log(satisfy { |ex| ex.to_s['Bad argument'] },
731
+ satisfy { |context| context['ExceptionHandlingError: log_error rescued exception while logging Runtime message'] },
732
+ anything)
733
+ ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
734
+ end
735
+
736
+ should "rescue exceptions that happen when log_error yields" do
737
+ mock(ExceptionHandling).write_exception_to_log(satisfy { |ex| ex.to_s['Bad argument'] },
738
+ satisfy { |context| context['Context message'] },
739
+ anything,
740
+ anything)
741
+ ExceptionHandling.log_error(ArgumentError.new("Bad argument"), "Context message") { |_data| raise 'Error!!!' }
742
+ end
743
+
744
+ context "Exception Filtering" do
745
+ setup do
746
+ filter_list = { exception1: { 'error' => "my error message" },
747
+ exception2: { 'error' => "some other message", :session => "misc data" } }
748
+ stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
749
+
750
+ # bump modified time up to get the above filter loaded
751
+ stub(File).mtime { incrementing_mtime }
752
+ end
753
+
754
+ should "handle case where filter list is not found" do
755
+ stub(YAML).load_file { raise Errno::ENOENT, "File not found" }
756
+
757
+ capture_notifications
758
+
759
+ ExceptionHandling.log_error("My error message is in list")
760
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
761
+ end
762
+
763
+ should "log exception and suppress email when exception is on filter list" do
764
+ capture_notifications
765
+
766
+ ExceptionHandling.log_error("Error message is not in list")
767
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
768
+
769
+ sent_notifications.clear
770
+ ExceptionHandling.log_error("My error message is in list")
771
+ assert_equal 0, sent_notifications.size, sent_notifications.inspect
772
+ end
773
+
774
+ should "allow filtering exception on any text in exception data" do
775
+ filters = { exception1: { session: "data: my extra session data" } }
776
+ stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filters) }
777
+
778
+ capture_notifications
779
+
780
+ ExceptionHandling.log_error("No match here") do |data|
781
+ data[:session] = {
782
+ key: "@session_id",
783
+ data: "my extra session data"
784
+ }
785
+ end
786
+ assert_equal 0, sent_notifications.size, sent_notifications.inspect
787
+
788
+ ExceptionHandling.log_error("No match here") do |data|
789
+ data[:session] = {
790
+ key: "@session_id",
791
+ data: "my extra session <no match!> data"
792
+ }
793
+ end
794
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
795
+ end
796
+
797
+ should "reload filter list on the next exception if file was modified" do
798
+ capture_notifications
799
+
800
+ ExceptionHandling.log_error("Error message is not in list")
801
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
802
+
803
+ filter_list = { exception1: { 'error' => "Error message is not in list" } }
804
+ stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
805
+ stub(File).mtime { incrementing_mtime }
806
+
807
+ sent_notifications.clear
808
+ ExceptionHandling.log_error("Error message is not in list")
809
+ assert_equal 0, sent_notifications.size, sent_notifications.inspect
810
+ end
811
+
812
+ should "not consider filter if both error message and body do not match" do
813
+ capture_notifications
814
+
815
+ # error message matches, but not full text
816
+ ExceptionHandling.log_error("some other message")
817
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
818
+
819
+ # now both match
820
+ sent_notifications.clear
821
+ ExceptionHandling.log_error("some other message") do |data|
822
+ data[:session] = { some_random_key: "misc data" }
823
+ end
824
+ assert_equal 0, sent_notifications.size, sent_notifications.inspect
825
+ end
826
+
827
+ should "skip environment keys not on whitelist" do
828
+ capture_notifications
829
+
830
+ ExceptionHandling.log_error("some message") do |data|
831
+ data[:environment] = { SERVER_PROTOCOL: "HTTP/1.0", RAILS_SECRETS_YML_CONTENTS: 'password: VERY_SECRET_PASSWORD' }
832
+ end
833
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
834
+
835
+ mail = sent_notifications.last
836
+ environment = mail.enhanced_data['environment']
837
+
838
+ assert_nil environment["RAILS_SECRETS_YML_CONTENTS"], environment.inspect # this is not on whitelist
839
+ assert environment["SERVER_PROTOCOL"], environment.inspect # this is
840
+ end
841
+
842
+ should "omit environment defaults" do
843
+ capture_notifications
844
+
845
+ stub(ExceptionHandling).send_exception_to_honeybadger(anything) { |exception_info| sent_notifications << exception_info }
846
+
847
+ ExceptionHandling.log_error("some message") do |data|
848
+ data[:environment] = { SERVER_PORT: '80', SERVER_PROTOCOL: "HTTP/1.0" }
849
+ end
850
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
851
+ mail = sent_notifications.last
852
+ environment = mail.enhanced_data['environment']
853
+
854
+ assert_nil environment["SERVER_PORT"], environment.inspect # this was default
855
+ assert environment["SERVER_PROTOCOL"], environment # this was not
856
+ end
857
+
858
+ should "reject the filter file if any contain all empty regexes" do
859
+ filter_list = { exception1: { 'error' => "", :session => "" },
860
+ exception2: { 'error' => "is not in list", :session => "" } }
861
+ stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
862
+ stub(File).mtime { incrementing_mtime }
863
+
864
+ capture_notifications
865
+
866
+ ExceptionHandling.log_error("Error message is not in list")
867
+ assert_equal 1, sent_notifications.size, sent_notifications.inspect
868
+ end
869
+
870
+ should "reload filter file if filename changes" do
871
+ catalog = ExceptionHandling.exception_catalog
872
+ ExceptionHandling.filter_list_filename = "./config/other_exception_filters.yml"
873
+ assert_not_equal catalog, ExceptionHandling.exception_catalog
874
+ end
875
+
876
+ context "Exception Handling Mailer" do
877
+ EXPECTED_SMTP_HASH =
878
+ {
879
+ host: '127.0.0.1',
880
+ domain: 'localhost.localdomain',
881
+ from: 'server@example.com',
882
+ to: 'escalation@example.com'
883
+ }.freeze
884
+
885
+ [[true, false], [true, true]].each do |em_flag, synchrony_flag|
886
+ context "eventmachine_safe = #{em_flag} && eventmachine_synchrony = #{synchrony_flag}" do
887
+ setup do
888
+ ExceptionHandling.eventmachine_safe = em_flag
889
+ ExceptionHandling.eventmachine_synchrony = synchrony_flag
890
+ EventMachineStub.block = nil
891
+ set_test_const('EventMachine', EventMachineStub)
892
+ set_test_const('EventMachine::Protocols', Module.new)
893
+ set_test_const('EventMachine::DNS', Module.new)
894
+ set_test_const('EventMachine::DNS::Resolver', DNSResolvStub)
895
+ end
896
+
897
+ teardown do
898
+ ExceptionHandling.eventmachine_safe = false
899
+ ExceptionHandling.eventmachine_synchrony = false
900
+ end
901
+
902
+ should "schedule EventMachine STMP when EventMachine defined" do
903
+ ActionMailer::Base.deliveries.clear
904
+
905
+ set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
906
+
907
+ ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
908
+ assert EventMachineStub.block
909
+ EventMachineStub.block.call
910
+ assert DNSResolvStub.callback_block
911
+ DNSResolvStub.callback_block.call ['127.0.0.1']
912
+ assert_equal_with_diff EXPECTED_SMTP_HASH, (SmtpClientStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }, SmtpClientStub.send_hash.inspect
913
+ assert_equal((synchrony_flag ? :asend : :send), SmtpClientStub.last_method)
914
+ assert_match(/Exception to escalate/, SmtpClientStub.send_hash[:content])
915
+ assert_emails 0, ActionMailer::Base.deliveries.*.to_s
916
+ end
917
+
918
+ should "pass the content as a proper rfc 2822 message" do
919
+ set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
920
+ ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
921
+ assert EventMachineStub.block
922
+ EventMachineStub.block.call
923
+ assert DNSResolvStub.callback_block
924
+ DNSResolvStub.callback_block.call ['127.0.0.1']
925
+ assert content = SmtpClientStub.send_hash[:content]
926
+ assert_match(/Content-Transfer-Encoding: 7bit/, content)
927
+ assert_match(/\r\n\.\r\n\z/, content)
928
+ end
929
+
930
+ should "log fatal on EventMachine STMP errback" do
931
+ ActionMailer::Base.deliveries.clear
932
+
933
+ set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientErrbackStub)
934
+ mock(ExceptionHandling.logger).fatal(/Exception to escalate/, anything)
935
+ mock(ExceptionHandling.logger).fatal(/Failed to email by SMTP: "credential mismatch"/)
936
+
937
+ ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
938
+ assert EventMachineStub.block
939
+ EventMachineStub.block.call
940
+ assert DNSResolvStub.callback_block
941
+ DNSResolvStub.callback_block.call(['127.0.0.1'])
942
+ SmtpClientErrbackStub.block.call("credential mismatch")
943
+ assert_equal_with_diff EXPECTED_SMTP_HASH, (SmtpClientErrbackStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }, SmtpClientErrbackStub.send_hash.inspect
944
+ end
945
+
946
+ should "log fatal on EventMachine dns resolver errback" do
947
+ mock(ExceptionHandling.logger).fatal(/Exception to escalate/, anything)
948
+ mock(ExceptionHandling.logger).fatal(/Failed to resolv DNS for localhost: "softlayer sucks"/)
949
+
950
+ ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
951
+ assert EventMachineStub.block
952
+ EventMachineStub.block.call
953
+ DNSResolvStub.errback_block.call("softlayer sucks")
954
+ end
955
+ end
956
+ end
957
+ end
958
+ end
959
+
960
+ context "Exception mapping" do
961
+ setup do
962
+ @data = {
963
+ environment: {
964
+ 'HTTP_HOST' => "localhost",
965
+ 'HTTP_REFERER' => "http://localhost/action/controller/instance",
966
+ },
967
+ session: {
968
+ data: {
969
+ affiliate_id: defined?(Affiliate) ? Affiliate.first.id : '1',
970
+ edit_mode: true,
971
+ advertiser_id: defined?(Advertiser) ? Advertiser.first.id : '1',
972
+ username_id: defined?(Username) ? Username.first.id : '1',
973
+ user_id: defined?(User) ? User.first.id : '1',
974
+ flash: {},
975
+ impersonated_organization_pk: 'Advertiser_1'
976
+ }
977
+ },
978
+ request: {},
979
+ backtrace: ["[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:580:in `call_filters'", "[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:601:in `run_before_filters'"],
980
+ api_key: "none",
981
+ error_class: "StandardError",
982
+ error: 'Some error message'
983
+ }
984
+ end
985
+
986
+ should "clean backtraces" do
987
+ begin
988
+ raise "test exception"
989
+ rescue => ex
990
+ backtrace = ex.backtrace
991
+ end
992
+ result = ExceptionHandling.send(:clean_backtrace, ex).to_s
993
+ assert_not_equal result, backtrace
994
+ end
995
+
996
+ should "return entire backtrace if cleaned is emtpy" do
997
+ begin
998
+ backtrace = ["/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:312:in `find_with_ids'",
999
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:107:in `find'",
1000
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `__send__'",
1001
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `find'",
1002
+ "/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `call'",
1003
+ "/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `test: Exception mapping should return entire backtrace if cleaned is emtpy. '",
1004
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `__send__'",
1005
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `run'",
1006
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:447:in `_run__1913317170__setup__4__callbacks'",
1007
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `send'",
1008
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `__run_callback'",
1009
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:385:in `_run_setup_callbacks'",
1010
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `send'",
1011
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `run_callbacks'",
1012
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:70:in `run'",
1013
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:34:in `run'",
1014
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `each'",
1015
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `run'",
1016
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnermediator.rb:46:in `old_run_suite'",
1017
+ "(eval):12:in `run_suite'",
1018
+ "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `send'",
1019
+ "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `start_mediator'",
1020
+ "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:81:in `start'",
1021
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnerutilities.rb:29:in `run'",
1022
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/autorunner.rb:12:in `run'",
1023
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit.rb:279",
1024
+ "-e:1"]
1025
+
1026
+ module ::Rails
1027
+ class BacktraceCleaner
1028
+ def clean(_backtrace)
1029
+ []
1030
+ end
1031
+ end
1032
+ end
1033
+
1034
+ mock(Rails).backtrace_cleaner { Rails::BacktraceCleaner.new }
1035
+
1036
+ ex = Exception.new
1037
+ ex.set_backtrace(backtrace)
1038
+ result = ExceptionHandling.send(:clean_backtrace, ex)
1039
+ assert_equal backtrace, result
1040
+ ensure
1041
+ Object.send(:remove_const, :Rails)
1042
+ end
1043
+ end
1044
+ end
1045
+
1046
+ context "log_perodically" do
1047
+ setup do
1048
+ Time.now_override = Time.now # Freeze time
1049
+ ExceptionHandling.logger.clear
1050
+ end
1051
+
1052
+ teardown do
1053
+ Time.now_override = nil
1054
+ end
1055
+
1056
+ should "take in additional logging context and pass them to the logger" do
1057
+ ExceptionHandling.log_periodically(:test_context_with_periodic, 30.minutes, "this will be written", service_name: 'exception_handling')
1058
+ assert_not_empty logged_excluding_reload_filter.last[:context]
1059
+ assert_equal({ service_name: 'exception_handling' }, logged_excluding_reload_filter.last[:context])
1060
+ end
1061
+
1062
+ should "log immediately when we are expected to log" do
1063
+ ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
1064
+ assert_equal 1, logged_excluding_reload_filter.size
1065
+
1066
+ Time.now_override = Time.now + 5.minutes
1067
+ ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will not be written")
1068
+ assert_equal 1, logged_excluding_reload_filter.size
1069
+
1070
+ ExceptionHandling.log_periodically(:test_another_periodic_exception, 30.minutes, "this will be written")
1071
+ assert_equal 2, logged_excluding_reload_filter.size
1072
+
1073
+ Time.now_override = Time.now + 26.minutes
1074
+
1075
+ ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
1076
+ assert_equal 3, logged_excluding_reload_filter.size
1077
+ end
1078
+ end
1079
+ end
1080
+
1081
+ private
1082
+
1083
+ def logged_excluding_reload_filter
1084
+ ExceptionHandling.logger.logged.select { |l| l[:message] !~ /Reloading filter list/ }
1085
+ end
1086
+
1087
+ def incrementing_mtime
1088
+ @mtime ||= Time.now
1089
+ @mtime += 1.day
1090
+ end
1091
+
1092
+ def exception_1
1093
+ @exception_1 ||=
1094
+ begin
1095
+ raise StandardError, "Exception 1"
1096
+ rescue => ex
1097
+ ex
1098
+ end
1099
+ end
1100
+
1101
+ def exception_2
1102
+ @exception_2 ||=
1103
+ begin
1104
+ raise StandardError, "Exception 2"
1105
+ rescue => ex
1106
+ ex
1107
+ end
1108
+ end
1109
+ end