activesupport 7.2.2.1 → 8.1.3

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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +422 -145
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +73 -2
  5. data/lib/active_support/benchmark.rb +21 -0
  6. data/lib/active_support/benchmarkable.rb +3 -2
  7. data/lib/active_support/broadcast_logger.rb +61 -74
  8. data/lib/active_support/cache/file_store.rb +14 -4
  9. data/lib/active_support/cache/mem_cache_store.rb +30 -29
  10. data/lib/active_support/cache/memory_store.rb +11 -5
  11. data/lib/active_support/cache/null_store.rb +2 -2
  12. data/lib/active_support/cache/redis_cache_store.rb +43 -34
  13. data/lib/active_support/cache/strategy/local_cache.rb +72 -27
  14. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  15. data/lib/active_support/cache.rb +88 -20
  16. data/lib/active_support/callbacks.rb +28 -13
  17. data/lib/active_support/class_attribute.rb +33 -0
  18. data/lib/active_support/code_generator.rb +9 -0
  19. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  20. data/lib/active_support/concurrency/share_lock.rb +0 -1
  21. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  22. data/lib/active_support/configurable.rb +34 -0
  23. data/lib/active_support/configuration_file.rb +15 -6
  24. data/lib/active_support/continuous_integration.rb +145 -0
  25. data/lib/active_support/core_ext/array/conversions.rb +3 -3
  26. data/lib/active_support/core_ext/array.rb +7 -7
  27. data/lib/active_support/core_ext/benchmark.rb +0 -15
  28. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  29. data/lib/active_support/core_ext/class/attribute.rb +26 -20
  30. data/lib/active_support/core_ext/class.rb +2 -2
  31. data/lib/active_support/core_ext/date/conversions.rb +2 -0
  32. data/lib/active_support/core_ext/date.rb +5 -5
  33. data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -35
  34. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  35. data/lib/active_support/core_ext/date_time/conversions.rb +4 -2
  36. data/lib/active_support/core_ext/date_time.rb +5 -5
  37. data/lib/active_support/core_ext/digest.rb +1 -1
  38. data/lib/active_support/core_ext/enumerable.rb +25 -8
  39. data/lib/active_support/core_ext/erb/util.rb +5 -5
  40. data/lib/active_support/core_ext/file.rb +1 -1
  41. data/lib/active_support/core_ext/hash/deep_merge.rb +1 -0
  42. data/lib/active_support/core_ext/hash/except.rb +0 -12
  43. data/lib/active_support/core_ext/hash.rb +8 -8
  44. data/lib/active_support/core_ext/integer.rb +3 -3
  45. data/lib/active_support/core_ext/kernel.rb +3 -3
  46. data/lib/active_support/core_ext/module/attr_internal.rb +3 -4
  47. data/lib/active_support/core_ext/module/introspection.rb +3 -0
  48. data/lib/active_support/core_ext/module.rb +11 -11
  49. data/lib/active_support/core_ext/numeric.rb +3 -3
  50. data/lib/active_support/core_ext/object/json.rb +24 -11
  51. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  52. data/lib/active_support/core_ext/object/try.rb +2 -2
  53. data/lib/active_support/core_ext/object.rb +13 -13
  54. data/lib/active_support/core_ext/pathname.rb +2 -2
  55. data/lib/active_support/core_ext/range/overlap.rb +3 -3
  56. data/lib/active_support/core_ext/range/sole.rb +17 -0
  57. data/lib/active_support/core_ext/range.rb +4 -4
  58. data/lib/active_support/core_ext/securerandom.rb +24 -8
  59. data/lib/active_support/core_ext/string/filters.rb +3 -3
  60. data/lib/active_support/core_ext/string/inflections.rb +1 -1
  61. data/lib/active_support/core_ext/string/multibyte.rb +12 -3
  62. data/lib/active_support/core_ext/string/output_safety.rb +29 -13
  63. data/lib/active_support/core_ext/string.rb +13 -13
  64. data/lib/active_support/core_ext/symbol.rb +1 -1
  65. data/lib/active_support/core_ext/thread/backtrace/location.rb +2 -7
  66. data/lib/active_support/core_ext/time/calculations.rb +7 -2
  67. data/lib/active_support/core_ext/time/compatibility.rb +2 -19
  68. data/lib/active_support/core_ext/time/conversions.rb +2 -0
  69. data/lib/active_support/core_ext/time.rb +5 -5
  70. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  71. data/lib/active_support/current_attributes.rb +27 -17
  72. data/lib/active_support/delegation.rb +25 -44
  73. data/lib/active_support/dependencies/interlock.rb +11 -5
  74. data/lib/active_support/dependencies.rb +6 -2
  75. data/lib/active_support/deprecation/reporting.rb +4 -21
  76. data/lib/active_support/deprecation.rb +1 -1
  77. data/lib/active_support/duration.rb +14 -10
  78. data/lib/active_support/editor.rb +70 -0
  79. data/lib/active_support/encrypted_configuration.rb +20 -2
  80. data/lib/active_support/error_reporter.rb +81 -4
  81. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  82. data/lib/active_support/event_reporter.rb +592 -0
  83. data/lib/active_support/evented_file_update_checker.rb +5 -2
  84. data/lib/active_support/execution_context.rb +75 -7
  85. data/lib/active_support/execution_wrapper.rb +1 -1
  86. data/lib/active_support/file_update_checker.rb +8 -6
  87. data/lib/active_support/gem_version.rb +4 -4
  88. data/lib/active_support/gzip.rb +1 -0
  89. data/lib/active_support/hash_with_indifferent_access.rb +61 -38
  90. data/lib/active_support/i18n_railtie.rb +19 -11
  91. data/lib/active_support/inflector/inflections.rb +34 -16
  92. data/lib/active_support/inflector/methods.rb +3 -3
  93. data/lib/active_support/inflector/transliterate.rb +6 -8
  94. data/lib/active_support/isolated_execution_state.rb +17 -17
  95. data/lib/active_support/json/decoding.rb +6 -4
  96. data/lib/active_support/json/encoding.rb +159 -21
  97. data/lib/active_support/lazy_load_hooks.rb +1 -1
  98. data/lib/active_support/log_subscriber.rb +2 -6
  99. data/lib/active_support/logger_thread_safe_level.rb +6 -3
  100. data/lib/active_support/message_encryptors.rb +54 -2
  101. data/lib/active_support/message_pack/extensions.rb +6 -1
  102. data/lib/active_support/message_verifier.rb +9 -0
  103. data/lib/active_support/message_verifiers.rb +57 -3
  104. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  105. data/lib/active_support/messages/rotator.rb +10 -0
  106. data/lib/active_support/multibyte/chars.rb +12 -2
  107. data/lib/active_support/multibyte.rb +4 -0
  108. data/lib/active_support/notifications/fanout.rb +64 -43
  109. data/lib/active_support/notifications/instrumenter.rb +1 -1
  110. data/lib/active_support/number_helper/number_converter.rb +1 -1
  111. data/lib/active_support/number_helper/number_to_delimited_converter.rb +17 -2
  112. data/lib/active_support/number_helper.rb +22 -0
  113. data/lib/active_support/railtie.rb +32 -9
  114. data/lib/active_support/structured_event_subscriber.rb +99 -0
  115. data/lib/active_support/subscriber.rb +0 -5
  116. data/lib/active_support/syntax_error_proxy.rb +7 -0
  117. data/lib/active_support/tagged_logging.rb +5 -0
  118. data/lib/active_support/test_case.rb +67 -6
  119. data/lib/active_support/testing/assertions.rb +118 -27
  120. data/lib/active_support/testing/autorun.rb +5 -0
  121. data/lib/active_support/testing/error_reporter_assertions.rb +17 -0
  122. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  123. data/lib/active_support/testing/isolation.rb +0 -2
  124. data/lib/active_support/testing/notification_assertions.rb +92 -0
  125. data/lib/active_support/testing/parallelization/server.rb +15 -2
  126. data/lib/active_support/testing/parallelization/worker.rb +9 -3
  127. data/lib/active_support/testing/parallelization.rb +25 -1
  128. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  129. data/lib/active_support/testing/time_helpers.rb +9 -4
  130. data/lib/active_support/time_with_zone.rb +36 -23
  131. data/lib/active_support/values/time_zone.rb +19 -10
  132. data/lib/active_support/xml_mini.rb +3 -2
  133. data/lib/active_support.rb +21 -9
  134. metadata +35 -16
  135. data/lib/active_support/core_ext/range/each.rb +0 -24
  136. data/lib/active_support/proxy_object.rb +0 -20
  137. data/lib/active_support/testing/strict_warnings.rb +0 -43
@@ -19,7 +19,7 @@ module ActiveSupport
19
19
  #
20
20
  # assert_not foo, 'foo should be false'
21
21
  def assert_not(object, message = nil)
22
- message ||= "Expected #{mu_pp(object)} to be nil or false"
22
+ message ||= -> { "Expected #{mu_pp(object)} to be nil or false" }
23
23
  assert !object, message
24
24
  end
25
25
 
@@ -71,19 +71,19 @@ module ActiveSupport
71
71
  # post :delete, params: { id: ... }
72
72
  # end
73
73
  #
74
- # An array of expressions can also be passed in and evaluated.
74
+ # An array of expressions can be passed in and evaluated.
75
75
  #
76
76
  # assert_difference [ 'Article.count', 'Post.count' ], 2 do
77
77
  # post :create, params: { article: {...} }
78
78
  # end
79
79
  #
80
- # A hash of expressions/numeric differences can also be passed in and evaluated.
80
+ # A hash of expressions/numeric differences can be passed in and evaluated.
81
81
  #
82
- # assert_difference ->{ Article.count } => 1, ->{ Notification.count } => 2 do
82
+ # assert_difference({ 'Article.count' => 1, 'Notification.count' => 2 }) do
83
83
  # post :create, params: { article: {...} }
84
84
  # end
85
85
  #
86
- # A lambda or a list of lambdas can be passed in and evaluated:
86
+ # A lambda, a list of lambdas or a hash of lambdas/numeric differences can be passed in and evaluated:
87
87
  #
88
88
  # assert_difference ->{ Article.count }, 2 do
89
89
  # post :create, params: { article: {...} }
@@ -93,6 +93,10 @@ module ActiveSupport
93
93
  # post :create, params: { article: {...} }
94
94
  # end
95
95
  #
96
+ # assert_difference ->{ Article.count } => 1, ->{ Notification.count } => 2 do
97
+ # post :create, params: { article: {...} }
98
+ # end
99
+ #
96
100
  # An error message can be specified.
97
101
  #
98
102
  # assert_difference 'Article.count', -1, 'An Article should be destroyed' do
@@ -118,9 +122,14 @@ module ActiveSupport
118
122
 
119
123
  expressions.zip(exps, before) do |(code, diff), exp, before_value|
120
124
  actual = exp.call
121
- error = "#{code.inspect} didn't change by #{diff}, but by #{actual - before_value}"
122
- error = "#{message}.\n#{error}" if message
123
- assert_equal(before_value + diff, actual, error)
125
+ rich_message = -> do
126
+ code_string = code.respond_to?(:call) ? _callable_to_source_string(code) : code
127
+ error = "`#{code_string}` didn't change by #{diff}, but by #{actual - before_value}."
128
+ error = "#{error}\n#{diff before_value + diff, actual}" if Minitest::VERSION > "6"
129
+ error = "#{message}.\n#{error}" if message
130
+ error
131
+ end
132
+ assert_equal(before_value + diff, actual, rich_message)
124
133
  end
125
134
 
126
135
  retval
@@ -177,12 +186,24 @@ module ActiveSupport
177
186
  #
178
187
  # The keyword arguments +:from+ and +:to+ can be given to specify the
179
188
  # expected initial value and the expected value after the block was
180
- # executed.
189
+ # executed. The comparison is done using case equality (===), which means
190
+ # you can specify patterns or classes:
181
191
  #
192
+ # # Exact value match
182
193
  # assert_changes :@object, from: nil, to: :foo do
183
194
  # @object = :foo
184
195
  # end
185
196
  #
197
+ # # Case equality
198
+ # assert_changes -> { user.token }, to: /\w{32}/ do
199
+ # user.generate_token
200
+ # end
201
+ #
202
+ # # Type check
203
+ # assert_changes -> { current_error }, from: nil, to: RuntimeError do
204
+ # raise "Oops"
205
+ # end
206
+ #
186
207
  # An error message can be specified.
187
208
  #
188
209
  # assert_changes -> { Status.all_good? }, 'Expected the status to be bad' do
@@ -195,22 +216,32 @@ module ActiveSupport
195
216
  retval = _assert_nothing_raised_or_warn("assert_changes", &block)
196
217
 
197
218
  unless from == UNTRACKED
198
- error = "Expected change from #{from.inspect}, got #{before.inspect}"
199
- error = "#{message}.\n#{error}" if message
200
- assert from === before, error
219
+ rich_message = -> do
220
+ error = "Expected change from #{from.inspect}, got #{before.inspect}"
221
+ error = "#{message}.\n#{error}" if message
222
+ error
223
+ end
224
+ assert from === before, rich_message
201
225
  end
202
226
 
203
227
  after = exp.call
204
228
 
205
- error = "#{expression.inspect} didn't change"
206
- error = "#{error}. It was already #{to.inspect}" if before == to
207
- error = "#{message}.\n#{error}" if message
208
- refute_equal before, after, error
229
+ rich_message = -> do
230
+ code_string = expression.respond_to?(:call) ? _callable_to_source_string(expression) : expression
231
+ error = "`#{code_string}` didn't change"
232
+ error = "#{error}. It was already #{to.inspect}." if before == to
233
+ error = "#{message}.\n#{error}" if message
234
+ error
235
+ end
236
+ refute_equal before, after, rich_message
209
237
 
210
238
  unless to == UNTRACKED
211
- error = "Expected change to #{to.inspect}, got #{after.inspect}\n"
212
- error = "#{message}.\n#{error}" if message
213
- assert to === after, error
239
+ rich_message = -> do
240
+ error = "Expected change to #{to.inspect}, got #{after.inspect}\n"
241
+ error = "#{message}.\n#{error}" if message
242
+ error
243
+ end
244
+ assert to === after, rich_message
214
245
  end
215
246
 
216
247
  retval
@@ -224,12 +255,24 @@ module ActiveSupport
224
255
  # end
225
256
  #
226
257
  # Provide the optional keyword argument +:from+ to specify the expected
227
- # initial value.
258
+ # initial value. The comparison is done using case equality (===), which means
259
+ # you can specify patterns or classes:
228
260
  #
261
+ # # Exact value match
229
262
  # assert_no_changes -> { Status.all_good? }, from: true do
230
263
  # post :create, params: { status: { ok: true } }
231
264
  # end
232
265
  #
266
+ # # Case equality
267
+ # assert_no_changes -> { user.token }, from: /\w{32}/ do
268
+ # user.touch
269
+ # end
270
+ #
271
+ # # Type check
272
+ # assert_no_changes -> { current_error }, from: RuntimeError do
273
+ # retry_operation
274
+ # end
275
+ #
233
276
  # An error message can be specified.
234
277
  #
235
278
  # assert_no_changes -> { Status.all_good? }, 'Expected the status to be good' do
@@ -242,20 +285,28 @@ module ActiveSupport
242
285
  retval = _assert_nothing_raised_or_warn("assert_no_changes", &block)
243
286
 
244
287
  unless from == UNTRACKED
245
- error = "Expected initial value of #{from.inspect}, got #{before.inspect}"
246
- error = "#{message}.\n#{error}" if message
247
- assert from === before, error
288
+ rich_message = -> do
289
+ error = "Expected initial value of #{from.inspect}, got #{before.inspect}"
290
+ error = "#{message}.\n#{error}" if message
291
+ error
292
+ end
293
+ assert from === before, rich_message
248
294
  end
249
295
 
250
296
  after = exp.call
251
297
 
252
- error = "#{expression.inspect} changed"
253
- error = "#{message}.\n#{error}" if message
298
+ rich_message = -> do
299
+ code_string = expression.respond_to?(:call) ? _callable_to_source_string(expression) : expression
300
+ error = "`#{code_string}` changed."
301
+ error = "#{message}.\n#{error}" if message
302
+ error = "#{error}\n#{diff before, after}" if Minitest::VERSION > "6"
303
+ error
304
+ end
254
305
 
255
306
  if before.nil?
256
- assert_nil after, error
307
+ assert_nil after, rich_message
257
308
  else
258
- assert_equal before, after, error
309
+ assert_equal before, after, rich_message
259
310
  end
260
311
 
261
312
  retval
@@ -276,6 +327,46 @@ module ActiveSupport
276
327
 
277
328
  raise
278
329
  end
330
+
331
+ def _callable_to_source_string(callable)
332
+ if defined?(RubyVM::InstructionSequence) && callable.is_a?(Proc)
333
+ iseq = RubyVM::InstructionSequence.of(callable)
334
+ source =
335
+ if iseq.script_lines
336
+ iseq.script_lines.join("\n")
337
+ elsif File.readable?(iseq.absolute_path)
338
+ File.read(iseq.absolute_path)
339
+ end
340
+
341
+ return callable unless source
342
+
343
+ location = iseq.to_a[4][:code_location]
344
+ return callable unless location
345
+
346
+ lines = source.lines[(location[0] - 1)..(location[2] - 1)]
347
+ lines[-1] = lines[-1].byteslice(...location[3])
348
+ lines[0] = lines[0].byteslice(location[1]...)
349
+ source = lines.join.strip
350
+
351
+ # Strip stabby lambda from Ruby 4.1+
352
+ source = source.sub(/^->\s*/, "")
353
+
354
+ # We ignore procs defined with do/end as they are likely multi-line anyway.
355
+ if source.start_with?("{")
356
+ source.delete_suffix!("}")
357
+ source.delete_prefix!("{")
358
+ source.strip!
359
+ # It won't read nice if the callable contains multiple
360
+ # lines, and it should be a rare occurrence anyway.
361
+ # Same if it takes arguments.
362
+ if !source.include?("\n") && !source.start_with?("|")
363
+ return source
364
+ end
365
+ end
366
+ end
367
+
368
+ callable
369
+ end
279
370
  end
280
371
  end
281
372
  end
@@ -2,4 +2,9 @@
2
2
 
3
3
  require "minitest"
4
4
 
5
+ # This respond_to check handles tests running sub-processes in an
6
+ # unbundled environment, which triggers MT5 usage. This conditional may
7
+ # be removable after the version bump, though it currently safeguards
8
+ # against issues in environments with multiple versions installed.
9
+ Minitest.load :rails if Minitest.respond_to? :load
5
10
  Minitest.autorun
@@ -102,6 +102,23 @@ module ActiveSupport
102
102
  assert(false, message)
103
103
  end
104
104
  end
105
+
106
+ # Captures reported errors from within the block that match the given
107
+ # error class.
108
+ #
109
+ # reports = capture_error_reports(IOError) do
110
+ # Rails.error.report(IOError.new("Oops"))
111
+ # Rails.error.report(IOError.new("Oh no"))
112
+ # Rails.error.report(StandardError.new)
113
+ # end
114
+ #
115
+ # assert_equal 2, reports.size
116
+ # assert_equal "Oops", reports.first.error.message
117
+ # assert_equal "Oh no", reports.last.error.message
118
+ def capture_error_reports(error_class = StandardError, &block)
119
+ reports = ErrorCollector.record(&block)
120
+ reports.select { |r| error_class === r.error }
121
+ end
105
122
  end
106
123
  end
107
124
  end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module Testing
5
+ # Provides test helpers for asserting on ActiveSupport::EventReporter events.
6
+ module EventReporterAssertions
7
+ module EventCollector # :nodoc:
8
+ @subscribed = false
9
+ @mutex = Mutex.new
10
+
11
+ class Event # :nodoc:
12
+ attr_reader :event_data
13
+
14
+ def initialize(event_data)
15
+ @event_data = event_data
16
+ end
17
+
18
+ def inspect
19
+ "#{event_data[:name]} (payload: #{event_data[:payload].inspect}, tags: #{event_data[:tags].inspect})"
20
+ end
21
+
22
+ def matches?(name, payload, tags)
23
+ return false unless name.to_s == event_data[:name]
24
+
25
+ if payload && payload.is_a?(Hash)
26
+ return false unless matches_hash?(payload, :payload)
27
+ end
28
+
29
+ return false unless matches_hash?(tags, :tags)
30
+ true
31
+ end
32
+
33
+ private
34
+ def matches_hash?(expected_hash, event_key)
35
+ expected_hash.all? do |k, v|
36
+ if v.is_a?(Regexp)
37
+ event_data.dig(event_key, k).to_s.match?(v)
38
+ else
39
+ event_data.dig(event_key, k) == v
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ class << self
46
+ def emit(event)
47
+ event_recorders&.each do |events|
48
+ events << Event.new(event)
49
+ end
50
+ true
51
+ end
52
+
53
+ def record
54
+ subscribe
55
+ events = []
56
+ event_recorders << events
57
+ begin
58
+ yield
59
+ events
60
+ ensure
61
+ event_recorders.delete_if { |r| events.equal?(r) }
62
+ end
63
+ end
64
+
65
+ private
66
+ def subscribe
67
+ return if @subscribed
68
+
69
+ @mutex.synchronize do
70
+ unless @subscribed
71
+ if ActiveSupport.event_reporter
72
+ ActiveSupport.event_reporter.subscribe(self)
73
+ @subscribed = true
74
+ else
75
+ raise Minitest::Assertion, "No event reporter is configured"
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ def event_recorders
82
+ ActiveSupport::IsolatedExecutionState[:active_support_event_reporter_assertions] ||= []
83
+ end
84
+ end
85
+ end
86
+
87
+ # Asserts that the block does not cause an event to be reported to +Rails.event+.
88
+ #
89
+ # If no name is provided, passes if evaluated code in the yielded block reports no events.
90
+ #
91
+ # assert_no_event_reported do
92
+ # service_that_does_not_report_events.perform
93
+ # end
94
+ #
95
+ # If a name is provided, passes if evaluated code in the yielded block reports no events
96
+ # with that name.
97
+ #
98
+ # assert_no_event_reported("user.created") do
99
+ # service_that_does_not_report_events.perform
100
+ # end
101
+ def assert_no_event_reported(name = nil, payload: {}, tags: {}, &block)
102
+ events = EventCollector.record(&block)
103
+
104
+ if name.nil?
105
+ assert_predicate(events, :empty?)
106
+ else
107
+ matching_event = events.find { |event| event.matches?(name, payload, tags) }
108
+ if matching_event
109
+ message = "Expected no '#{name}' event to be reported, but found:\n " \
110
+ "#{matching_event.inspect}"
111
+ flunk(message)
112
+ end
113
+ assert(true)
114
+ end
115
+ end
116
+
117
+ # Asserts that the block causes an event with the given name to be reported
118
+ # to +Rails.event+.
119
+ #
120
+ # Passes if the evaluated code in the yielded block reports a matching event.
121
+ #
122
+ # assert_event_reported("user.created") do
123
+ # Rails.event.notify("user.created", { id: 123 })
124
+ # end
125
+ #
126
+ # To test further details about the reported event, you can specify payload and tag matchers.
127
+ #
128
+ # assert_event_reported("user.created",
129
+ # payload: { id: 123, name: "John Doe" },
130
+ # tags: { request_id: /[0-9]+/ }
131
+ # ) do
132
+ # Rails.event.tagged(request_id: "123") do
133
+ # Rails.event.notify("user.created", { id: 123, name: "John Doe" })
134
+ # end
135
+ # end
136
+ #
137
+ # The matchers support partial matching - only the specified keys need to match.
138
+ #
139
+ # assert_event_reported("user.created", payload: { id: 123 }) do
140
+ # Rails.event.notify("user.created", { id: 123, name: "John Doe" })
141
+ # end
142
+ def assert_event_reported(name, payload: nil, tags: {}, &block)
143
+ events = EventCollector.record(&block)
144
+
145
+ if events.empty?
146
+ flunk("Expected an event to be reported, but there were no events reported.")
147
+ elsif (event = events.find { |event| event.matches?(name, payload, tags) })
148
+ assert(true)
149
+ event.event_data
150
+ else
151
+ message = "Expected an event to be reported matching:\n " \
152
+ "name: #{name.inspect}\n " \
153
+ "payload: #{payload.inspect}\n " \
154
+ "tags: #{tags.inspect}\n" \
155
+ "but none of the #{events.size} reported events matched:\n " \
156
+ "#{events.map(&:inspect).join("\n ")}"
157
+ flunk(message)
158
+ end
159
+ end
160
+
161
+ # Asserts that the provided events were reported, regardless of order.
162
+ #
163
+ # assert_events_reported([
164
+ # { name: "user.created", payload: { id: 123 } },
165
+ # { name: "email.sent", payload: { to: "user@example.com" } }
166
+ # ]) do
167
+ # create_user_and_send_welcome_email
168
+ # end
169
+ #
170
+ # Supports the same payload and tag matching as +assert_event_reported+.
171
+ #
172
+ # assert_events_reported([
173
+ # {
174
+ # name: "process.started",
175
+ # payload: { id: 123 },
176
+ # tags: { request_id: /[0-9]+/ }
177
+ # },
178
+ # { name: "process.completed" }
179
+ # ]) do
180
+ # Rails.event.tagged(request_id: "456") do
181
+ # start_and_complete_process(123)
182
+ # end
183
+ # end
184
+ def assert_events_reported(expected_events, &block)
185
+ events = EventCollector.record(&block)
186
+
187
+ if events.empty? && expected_events.size > 0
188
+ flunk("Expected #{expected_events.size} events to be reported, but there were no events reported.")
189
+ end
190
+
191
+ events_copy = events.dup
192
+
193
+ expected_events.each do |expected_event|
194
+ name = expected_event[:name]
195
+ payload = expected_event[:payload] || {}
196
+ tags = expected_event[:tags] || {}
197
+
198
+ matching_event_index = events_copy.find_index { |event| event.matches?(name, payload, tags) }
199
+
200
+ if matching_event_index
201
+ events_copy.delete_at(matching_event_index)
202
+ else
203
+ message = "Expected an event to be reported matching:\n " \
204
+ "name: #{name.inspect}\n " \
205
+ "payload: #{payload.inspect}\n " \
206
+ "tags: #{tags.inspect}\n" \
207
+ "but none of the #{events.size} reported events matched:\n " \
208
+ "#{events.map(&:inspect).join("\n ")}"
209
+ flunk(message)
210
+ end
211
+ end
212
+
213
+ assert(true)
214
+ end
215
+
216
+ # Allows debug events to be reported to +Rails.event+ for the duration of a given block.
217
+ #
218
+ # with_debug_event_reporting do
219
+ # service_that_reports_debug_events.perform
220
+ # end
221
+ #
222
+ def with_debug_event_reporting(&block)
223
+ ActiveSupport.event_reporter.with_debug(&block)
224
+ end
225
+ end
226
+ end
227
+ end
@@ -5,8 +5,6 @@ require "active_support/testing/parallelize_executor"
5
5
  module ActiveSupport
6
6
  module Testing
7
7
  module Isolation
8
- require "thread"
9
-
10
8
  SubprocessCrashed = Class.new(StandardError)
11
9
 
12
10
  def self.included(klass) # :nodoc:
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module Testing
5
+ module NotificationAssertions
6
+ # Assert a notification was emitted with a given +pattern+ and optional +payload+.
7
+ #
8
+ # You can assert that a notification was emitted by passing a pattern, which accepts
9
+ # either a string or regexp, an optional payload, and a block. While the block
10
+ # is executed, if a matching notification is emitted, the assertion will pass
11
+ # and the notification will be returned.
12
+ #
13
+ # Note that the payload is matched as a subset, meaning that the notification must
14
+ # contain at least the specified keys and values, but may contain additional ones.
15
+ #
16
+ # assert_notification("post.submitted", title: "Cool Post") do
17
+ # post.submit(title: "Cool Post", body: "Cool Body") # => emits matching notification
18
+ # end
19
+ #
20
+ # Using the returned notification, you can make more customized assertions.
21
+ #
22
+ # notification = assert_notification("post.submitted", title: "Cool Post") do
23
+ # ActiveSupport::Notifications.instrument("post.submitted", title: "Cool Post", body: Body.new("Cool Body"))
24
+ # end
25
+ #
26
+ # assert_instance_of(Body, notification.payload[:body])
27
+ #
28
+ def assert_notification(pattern, payload = nil, &block)
29
+ notifications = capture_notifications(pattern, &block)
30
+ assert_not_empty(notifications, "No #{pattern} notifications were found")
31
+
32
+ return notifications.first if payload.nil?
33
+
34
+ notification = notifications.find { |notification| notification.payload.slice(*payload.keys) == payload }
35
+ assert_not_nil(notification, "No #{pattern} notification with payload #{payload} was found")
36
+
37
+ notification
38
+ end
39
+
40
+ # Assert the number of notifications emitted with a given +pattern+.
41
+ #
42
+ # You can assert the number of notifications emitted by passing a pattern, which accepts
43
+ # either a string or regexp, a count, and a block. While the block is executed,
44
+ # the number of matching notifications emitted will be counted. After the block's
45
+ # execution completes, the assertion will pass if the count matches.
46
+ #
47
+ # assert_notifications_count("post.submitted", 1) do
48
+ # post.submit(title: "Cool Post") # => emits matching notification
49
+ # end
50
+ #
51
+ def assert_notifications_count(pattern, count, &block)
52
+ actual_count = capture_notifications(pattern, &block).count
53
+ assert_equal(count, actual_count, "Expected #{count} instead of #{actual_count} notifications for #{pattern}")
54
+ end
55
+
56
+ # Assert no notifications were emitted for a given +pattern+.
57
+ #
58
+ # You can assert no notifications were emitted by passing a pattern, which accepts
59
+ # either a string or regexp, and a block. While the block is executed, if no
60
+ # matching notifications are emitted, the assertion will pass.
61
+ #
62
+ # assert_no_notifications("post.submitted") do
63
+ # post.destroy # => emits non-matching notification
64
+ # end
65
+ #
66
+ def assert_no_notifications(pattern = nil, &block)
67
+ notifications = capture_notifications(pattern, &block)
68
+ error_message = if pattern
69
+ "Expected no notifications for #{pattern} but found #{notifications.size}"
70
+ else
71
+ "Expected no notifications but found #{notifications.size}"
72
+ end
73
+ assert_empty(notifications, error_message)
74
+ end
75
+
76
+ # Capture emitted notifications, optionally filtered by a +pattern+.
77
+ #
78
+ # You can capture emitted notifications, optionally filtered by a pattern,
79
+ # which accepts either a string or regexp, and a block.
80
+ #
81
+ # notifications = capture_notifications("post.submitted") do
82
+ # post.submit(title: "Cool Post") # => emits matching notification
83
+ # end
84
+ #
85
+ def capture_notifications(pattern = nil, &block)
86
+ notifications = []
87
+ ActiveSupport::Notifications.subscribed(->(n) { notifications << n }, pattern, &block)
88
+ notifications
89
+ end
90
+ end
91
+ end
92
+ end
@@ -14,6 +14,7 @@ module ActiveSupport
14
14
  def initialize
15
15
  @queue = Queue.new
16
16
  @active_workers = Concurrent::Map.new
17
+ @worker_pids = Concurrent::Map.new
17
18
  @in_flight = Concurrent::Map.new
18
19
  end
19
20
 
@@ -40,12 +41,24 @@ module ActiveSupport
40
41
  end
41
42
  end
42
43
 
43
- def start_worker(worker_id)
44
+ def start_worker(worker_id, worker_pid)
44
45
  @active_workers[worker_id] = true
46
+ @worker_pids[worker_id] = worker_pid
45
47
  end
46
48
 
47
- def stop_worker(worker_id)
49
+ def stop_worker(worker_id, worker_pid)
48
50
  @active_workers.delete(worker_id)
51
+ @worker_pids.delete(worker_id)
52
+ end
53
+
54
+ def remove_dead_workers(dead_pids)
55
+ dead_pids.each do |dead_pid|
56
+ worker_id = @worker_pids.key(dead_pid)
57
+ if worker_id
58
+ @active_workers.delete(worker_id)
59
+ @worker_pids.delete(worker_id)
60
+ end
61
+ end
49
62
  end
50
63
 
51
64
  def active_workers?
@@ -18,7 +18,7 @@ module ActiveSupport
18
18
  DRb.stop_service
19
19
 
20
20
  @queue = DRbObject.new_with_uri(@url)
21
- @queue.start_worker(@id)
21
+ @queue.start_worker(@id, Process.pid)
22
22
 
23
23
  begin
24
24
  after_fork
@@ -29,7 +29,7 @@ module ActiveSupport
29
29
  set_process_title("(stopping)")
30
30
 
31
31
  run_cleanup
32
- @queue.stop_worker(@id)
32
+ @queue.stop_worker(@id, Process.pid)
33
33
  end
34
34
  end
35
35
 
@@ -47,7 +47,11 @@ module ActiveSupport
47
47
  set_process_title("#{klass}##{method}")
48
48
 
49
49
  result = klass.with_info_handler reporter do
50
- Minitest.run_one_method(klass, method)
50
+ if Minitest.respond_to? :run_one_method
51
+ Minitest.run_one_method klass, method
52
+ else
53
+ klass.new(method).run
54
+ end
51
55
  end
52
56
 
53
57
  safe_record(reporter, result)
@@ -78,6 +82,8 @@ module ActiveSupport
78
82
  end
79
83
 
80
84
  def after_fork
85
+ ActiveSupport::TestCase.parallel_worker_id = @number
86
+
81
87
  Parallelization.after_fork_hooks.each do |cb|
82
88
  cb.call(@number)
83
89
  end