activesupport 7.1.6 → 8.1.1

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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +256 -1133
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/array_inquirer.rb +1 -1
  5. data/lib/active_support/backtrace_cleaner.rb +81 -3
  6. data/lib/active_support/benchmark.rb +21 -0
  7. data/lib/active_support/benchmarkable.rb +3 -2
  8. data/lib/active_support/broadcast_logger.rb +65 -78
  9. data/lib/active_support/cache/file_store.rb +29 -14
  10. data/lib/active_support/cache/mem_cache_store.rb +42 -102
  11. data/lib/active_support/cache/memory_store.rb +11 -6
  12. data/lib/active_support/cache/null_store.rb +2 -2
  13. data/lib/active_support/cache/redis_cache_store.rb +58 -46
  14. data/lib/active_support/cache/serializer_with_fallback.rb +0 -23
  15. data/lib/active_support/cache/strategy/local_cache.rb +72 -27
  16. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  17. data/lib/active_support/cache.rb +146 -86
  18. data/lib/active_support/callbacks.rb +102 -126
  19. data/lib/active_support/class_attribute.rb +33 -0
  20. data/lib/active_support/code_generator.rb +9 -0
  21. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  22. data/lib/active_support/concurrency/share_lock.rb +0 -1
  23. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  24. data/lib/active_support/configurable.rb +34 -0
  25. data/lib/active_support/configuration_file.rb +15 -6
  26. data/lib/active_support/continuous_integration.rb +145 -0
  27. data/lib/active_support/core_ext/array/conversions.rb +3 -5
  28. data/lib/active_support/core_ext/array.rb +7 -7
  29. data/lib/active_support/core_ext/benchmark.rb +4 -14
  30. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  31. data/lib/active_support/core_ext/class/attribute.rb +26 -19
  32. data/lib/active_support/core_ext/class/subclasses.rb +15 -35
  33. data/lib/active_support/core_ext/class.rb +2 -2
  34. data/lib/active_support/core_ext/date/blank.rb +4 -0
  35. data/lib/active_support/core_ext/date/conversions.rb +2 -2
  36. data/lib/active_support/core_ext/date.rb +5 -5
  37. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -9
  38. data/lib/active_support/core_ext/date_time/blank.rb +4 -0
  39. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  40. data/lib/active_support/core_ext/date_time/conversions.rb +4 -6
  41. data/lib/active_support/core_ext/date_time.rb +5 -5
  42. data/lib/active_support/core_ext/digest/uuid.rb +6 -0
  43. data/lib/active_support/core_ext/digest.rb +1 -1
  44. data/lib/active_support/core_ext/enumerable.rb +25 -8
  45. data/lib/active_support/core_ext/erb/util.rb +10 -5
  46. data/lib/active_support/core_ext/file.rb +1 -1
  47. data/lib/active_support/core_ext/hash/deep_merge.rb +1 -0
  48. data/lib/active_support/core_ext/hash/except.rb +0 -12
  49. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  50. data/lib/active_support/core_ext/hash.rb +8 -8
  51. data/lib/active_support/core_ext/integer.rb +3 -3
  52. data/lib/active_support/core_ext/kernel.rb +3 -3
  53. data/lib/active_support/core_ext/module/attr_internal.rb +16 -6
  54. data/lib/active_support/core_ext/module/delegation.rb +20 -163
  55. data/lib/active_support/core_ext/module/deprecation.rb +1 -4
  56. data/lib/active_support/core_ext/module/introspection.rb +3 -0
  57. data/lib/active_support/core_ext/module.rb +11 -11
  58. data/lib/active_support/core_ext/numeric/conversions.rb +3 -3
  59. data/lib/active_support/core_ext/numeric.rb +3 -3
  60. data/lib/active_support/core_ext/object/blank.rb +45 -1
  61. data/lib/active_support/core_ext/object/instance_variables.rb +11 -19
  62. data/lib/active_support/core_ext/object/json.rb +24 -11
  63. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  64. data/lib/active_support/core_ext/object/try.rb +2 -2
  65. data/lib/active_support/core_ext/object/with.rb +5 -3
  66. data/lib/active_support/core_ext/object.rb +13 -13
  67. data/lib/active_support/core_ext/pathname/blank.rb +4 -0
  68. data/lib/active_support/core_ext/pathname.rb +2 -2
  69. data/lib/active_support/core_ext/range/overlap.rb +4 -4
  70. data/lib/active_support/core_ext/range/sole.rb +17 -0
  71. data/lib/active_support/core_ext/range.rb +4 -4
  72. data/lib/active_support/core_ext/securerandom.rb +4 -4
  73. data/lib/active_support/core_ext/string/conversions.rb +1 -1
  74. data/lib/active_support/core_ext/string/filters.rb +4 -4
  75. data/lib/active_support/core_ext/string/multibyte.rb +13 -4
  76. data/lib/active_support/core_ext/string/output_safety.rb +19 -19
  77. data/lib/active_support/core_ext/string.rb +13 -13
  78. data/lib/active_support/core_ext/symbol.rb +1 -1
  79. data/lib/active_support/core_ext/thread/backtrace/location.rb +2 -7
  80. data/lib/active_support/core_ext/time/calculations.rb +25 -30
  81. data/lib/active_support/core_ext/time/compatibility.rb +2 -3
  82. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  83. data/lib/active_support/core_ext/time/zones.rb +1 -1
  84. data/lib/active_support/core_ext/time.rb +5 -5
  85. data/lib/active_support/core_ext.rb +1 -2
  86. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  87. data/lib/active_support/current_attributes.rb +58 -50
  88. data/lib/active_support/delegation.rb +200 -0
  89. data/lib/active_support/dependencies/autoload.rb +0 -12
  90. data/lib/active_support/dependencies/interlock.rb +11 -5
  91. data/lib/active_support/dependencies.rb +6 -2
  92. data/lib/active_support/deprecation/constant_accessor.rb +47 -26
  93. data/lib/active_support/deprecation/proxy_wrappers.rb +9 -12
  94. data/lib/active_support/deprecation/reporting.rb +5 -17
  95. data/lib/active_support/deprecation.rb +8 -5
  96. data/lib/active_support/descendants_tracker.rb +9 -87
  97. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  98. data/lib/active_support/duration/iso8601_serializer.rb +1 -2
  99. data/lib/active_support/duration.rb +25 -16
  100. data/lib/active_support/editor.rb +70 -0
  101. data/lib/active_support/encrypted_configuration.rb +20 -2
  102. data/lib/active_support/encrypted_file.rb +1 -1
  103. data/lib/active_support/error_reporter.rb +121 -6
  104. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  105. data/lib/active_support/event_reporter.rb +592 -0
  106. data/lib/active_support/evented_file_update_checker.rb +5 -3
  107. data/lib/active_support/execution_context.rb +64 -7
  108. data/lib/active_support/execution_wrapper.rb +1 -2
  109. data/lib/active_support/file_update_checker.rb +9 -7
  110. data/lib/active_support/fork_tracker.rb +2 -38
  111. data/lib/active_support/gem_version.rb +2 -2
  112. data/lib/active_support/gzip.rb +1 -0
  113. data/lib/active_support/hash_with_indifferent_access.rb +66 -45
  114. data/lib/active_support/html_safe_translation.rb +3 -0
  115. data/lib/active_support/i18n_railtie.rb +19 -11
  116. data/lib/active_support/inflector/inflections.rb +31 -15
  117. data/lib/active_support/inflector/transliterate.rb +6 -8
  118. data/lib/active_support/isolated_execution_state.rb +12 -17
  119. data/lib/active_support/json/decoding.rb +6 -4
  120. data/lib/active_support/json/encoding.rb +157 -21
  121. data/lib/active_support/lazy_load_hooks.rb +1 -1
  122. data/lib/active_support/log_subscriber.rb +2 -18
  123. data/lib/active_support/logger.rb +15 -2
  124. data/lib/active_support/logger_thread_safe_level.rb +4 -9
  125. data/lib/active_support/message_encryptors.rb +54 -2
  126. data/lib/active_support/message_pack/extensions.rb +20 -2
  127. data/lib/active_support/message_verifier.rb +21 -0
  128. data/lib/active_support/message_verifiers.rb +57 -3
  129. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  130. data/lib/active_support/messages/rotator.rb +10 -0
  131. data/lib/active_support/multibyte/chars.rb +14 -4
  132. data/lib/active_support/multibyte.rb +4 -0
  133. data/lib/active_support/notifications/fanout.rb +68 -50
  134. data/lib/active_support/notifications/instrumenter.rb +22 -19
  135. data/lib/active_support/notifications.rb +28 -27
  136. data/lib/active_support/number_helper/number_converter.rb +2 -2
  137. data/lib/active_support/number_helper.rb +22 -0
  138. data/lib/active_support/option_merger.rb +2 -2
  139. data/lib/active_support/ordered_options.rb +53 -15
  140. data/lib/active_support/railtie.rb +36 -20
  141. data/lib/active_support/string_inquirer.rb +1 -1
  142. data/lib/active_support/structured_event_subscriber.rb +99 -0
  143. data/lib/active_support/subscriber.rb +1 -5
  144. data/lib/active_support/syntax_error_proxy.rb +3 -0
  145. data/lib/active_support/tagged_logging.rb +5 -1
  146. data/lib/active_support/test_case.rb +63 -6
  147. data/lib/active_support/testing/assertions.rb +113 -27
  148. data/lib/active_support/testing/constant_stubbing.rb +30 -8
  149. data/lib/active_support/testing/deprecation.rb +5 -12
  150. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  151. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  152. data/lib/active_support/testing/isolation.rb +19 -9
  153. data/lib/active_support/testing/method_call_assertions.rb +2 -16
  154. data/lib/active_support/testing/notification_assertions.rb +92 -0
  155. data/lib/active_support/testing/parallelization/server.rb +18 -2
  156. data/lib/active_support/testing/parallelization/worker.rb +4 -2
  157. data/lib/active_support/testing/parallelization.rb +25 -1
  158. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  159. data/lib/active_support/testing/time_helpers.rb +11 -6
  160. data/lib/active_support/time_with_zone.rb +39 -26
  161. data/lib/active_support/values/time_zone.rb +26 -17
  162. data/lib/active_support/xml_mini.rb +14 -4
  163. data/lib/active_support.rb +22 -9
  164. metadata +31 -17
  165. data/lib/active_support/core_ext/range/each.rb +0 -24
  166. data/lib/active_support/deprecation/instance_delegator.rb +0 -65
  167. data/lib/active_support/proxy_object.rb +0 -17
  168. data/lib/active_support/ruby_features.rb +0 -7
  169. data/lib/active_support/testing/strict_warnings.rb +0 -39
@@ -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,13 @@ 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 = "#{message}.\n#{error}" if message
129
+ error
130
+ end
131
+ assert_equal(before_value + diff, actual, rich_message)
124
132
  end
125
133
 
126
134
  retval
@@ -177,12 +185,24 @@ module ActiveSupport
177
185
  #
178
186
  # The keyword arguments +:from+ and +:to+ can be given to specify the
179
187
  # expected initial value and the expected value after the block was
180
- # executed.
188
+ # executed. The comparison is done using case equality (===), which means
189
+ # you can specify patterns or classes:
181
190
  #
191
+ # # Exact value match
182
192
  # assert_changes :@object, from: nil, to: :foo do
183
193
  # @object = :foo
184
194
  # end
185
195
  #
196
+ # # Case equality
197
+ # assert_changes -> { user.token }, to: /\w{32}/ do
198
+ # user.generate_token
199
+ # end
200
+ #
201
+ # # Type check
202
+ # assert_changes -> { current_error }, from: nil, to: RuntimeError do
203
+ # raise "Oops"
204
+ # end
205
+ #
186
206
  # An error message can be specified.
187
207
  #
188
208
  # assert_changes -> { Status.all_good? }, 'Expected the status to be bad' do
@@ -195,22 +215,32 @@ module ActiveSupport
195
215
  retval = _assert_nothing_raised_or_warn("assert_changes", &block)
196
216
 
197
217
  unless from == UNTRACKED
198
- error = "Expected change from #{from.inspect}, got #{before}"
199
- error = "#{message}.\n#{error}" if message
200
- assert from === before, error
218
+ rich_message = -> do
219
+ error = "Expected change from #{from.inspect}, got #{before.inspect}"
220
+ error = "#{message}.\n#{error}" if message
221
+ error
222
+ end
223
+ assert from === before, rich_message
201
224
  end
202
225
 
203
226
  after = exp.call
204
227
 
205
- error = "#{expression.inspect} didn't change"
206
- error = "#{error}. It was already #{to}" if before == to
207
- error = "#{message}.\n#{error}" if message
208
- refute_equal before, after, error
228
+ rich_message = -> do
229
+ code_string = expression.respond_to?(:call) ? _callable_to_source_string(expression) : expression
230
+ error = "`#{code_string}` didn't change"
231
+ error = "#{error}. It was already #{to.inspect}" if before == to
232
+ error = "#{message}.\n#{error}" if message
233
+ error
234
+ end
235
+ refute_equal before, after, rich_message
209
236
 
210
237
  unless to == UNTRACKED
211
- error = "Expected change to #{to}, got #{after}\n"
212
- error = "#{message}.\n#{error}" if message
213
- assert to === after, error
238
+ rich_message = -> do
239
+ error = "Expected change to #{to.inspect}, got #{after.inspect}\n"
240
+ error = "#{message}.\n#{error}" if message
241
+ error
242
+ end
243
+ assert to === after, rich_message
214
244
  end
215
245
 
216
246
  retval
@@ -224,12 +254,24 @@ module ActiveSupport
224
254
  # end
225
255
  #
226
256
  # Provide the optional keyword argument +:from+ to specify the expected
227
- # initial value.
257
+ # initial value. The comparison is done using case equality (===), which means
258
+ # you can specify patterns or classes:
228
259
  #
260
+ # # Exact value match
229
261
  # assert_no_changes -> { Status.all_good? }, from: true do
230
262
  # post :create, params: { status: { ok: true } }
231
263
  # end
232
264
  #
265
+ # # Case equality
266
+ # assert_no_changes -> { user.token }, from: /\w{32}/ do
267
+ # user.touch
268
+ # end
269
+ #
270
+ # # Type check
271
+ # assert_no_changes -> { current_error }, from: RuntimeError do
272
+ # retry_operation
273
+ # end
274
+ #
233
275
  # An error message can be specified.
234
276
  #
235
277
  # assert_no_changes -> { Status.all_good? }, 'Expected the status to be good' do
@@ -242,20 +284,27 @@ module ActiveSupport
242
284
  retval = _assert_nothing_raised_or_warn("assert_no_changes", &block)
243
285
 
244
286
  unless from == UNTRACKED
245
- error = "Expected initial value of #{from.inspect}"
246
- error = "#{message}.\n#{error}" if message
247
- assert from === before, error
287
+ rich_message = -> do
288
+ error = "Expected initial value of #{from.inspect}, got #{before.inspect}"
289
+ error = "#{message}.\n#{error}" if message
290
+ error
291
+ end
292
+ assert from === before, rich_message
248
293
  end
249
294
 
250
295
  after = exp.call
251
296
 
252
- error = "#{expression.inspect} changed"
253
- error = "#{message}.\n#{error}" if message
297
+ rich_message = -> do
298
+ code_string = expression.respond_to?(:call) ? _callable_to_source_string(expression) : expression
299
+ error = "`#{code_string}` changed"
300
+ error = "#{message}.\n#{error}" if message
301
+ error
302
+ end
254
303
 
255
304
  if before.nil?
256
- assert_nil after, error
305
+ assert_nil after, rich_message
257
306
  else
258
- assert_equal before, after, error
307
+ assert_equal before, after, rich_message
259
308
  end
260
309
 
261
310
  retval
@@ -276,6 +325,43 @@ module ActiveSupport
276
325
 
277
326
  raise
278
327
  end
328
+
329
+ def _callable_to_source_string(callable)
330
+ if defined?(RubyVM::InstructionSequence) && callable.is_a?(Proc)
331
+ iseq = RubyVM::InstructionSequence.of(callable)
332
+ source =
333
+ if iseq.script_lines
334
+ iseq.script_lines.join("\n")
335
+ elsif File.readable?(iseq.absolute_path)
336
+ File.read(iseq.absolute_path)
337
+ end
338
+
339
+ return callable unless source
340
+
341
+ location = iseq.to_a[4][:code_location]
342
+ return callable unless location
343
+
344
+ lines = source.lines[(location[0] - 1)..(location[2] - 1)]
345
+ lines[-1] = lines[-1].byteslice(...location[3])
346
+ lines[0] = lines[0].byteslice(location[1]...)
347
+ source = lines.join.strip
348
+
349
+ # We ignore procs defined with do/end as they are likely multi-line anyway.
350
+ if source.start_with?("{")
351
+ source.delete_suffix!("}")
352
+ source.delete_prefix!("{")
353
+ source.strip!
354
+ # It won't read nice if the callable contains multiple
355
+ # lines, and it should be a rare occurrence anyway.
356
+ # Same if it takes arguments.
357
+ if !source.include?("\n") && !source.start_with?("|")
358
+ return source
359
+ end
360
+ end
361
+ end
362
+
363
+ callable
364
+ end
279
365
  end
280
366
  end
281
367
  end
@@ -15,17 +15,39 @@ module ActiveSupport
15
15
  # Using this method rather than forcing <tt>World::List::Import::LARGE_IMPORT_THRESHOLD = 5000</tt> prevents
16
16
  # warnings from being thrown, and ensures that the old value is returned after the test has completed.
17
17
  #
18
+ # If the constant doesn't already exists, but you need it set for the duration of the block
19
+ # you can do so by passing `exists: false`.
20
+ #
21
+ # stub_const(object, :SOME_CONST, 1, exists: false) do
22
+ # assert_equal 1, SOME_CONST
23
+ # end
24
+ #
18
25
  # Note: Stubbing a const will stub it across all threads. So if you have concurrent threads
19
26
  # (like separate test suites running in parallel) that all depend on the same constant, it's possible
20
27
  # divergent stubbing will trample on each other.
21
- def stub_const(mod, constant, new_value)
22
- old_value = mod.const_get(constant, false)
23
- mod.send(:remove_const, constant)
24
- mod.const_set(constant, new_value)
25
- yield
26
- ensure
27
- mod.send(:remove_const, constant)
28
- mod.const_set(constant, old_value)
28
+ def stub_const(mod, constant, new_value, exists: true)
29
+ if exists
30
+ begin
31
+ old_value = mod.const_get(constant, false)
32
+ mod.send(:remove_const, constant)
33
+ mod.const_set(constant, new_value)
34
+ yield
35
+ ensure
36
+ mod.send(:remove_const, constant)
37
+ mod.const_set(constant, old_value)
38
+ end
39
+ else
40
+ if mod.const_defined?(constant)
41
+ raise NameError, "already defined constant #{constant} in #{mod.name}"
42
+ end
43
+
44
+ begin
45
+ mod.const_set(constant, new_value)
46
+ yield
47
+ ensure
48
+ mod.send(:remove_const, constant)
49
+ end
50
+ end
29
51
  end
30
52
  end
31
53
  end
@@ -29,10 +29,11 @@ module ActiveSupport
29
29
  # end
30
30
  def assert_deprecated(match = nil, deprecator = nil, &block)
31
31
  match, deprecator = nil, match if match.is_a?(ActiveSupport::Deprecation)
32
+
32
33
  unless deprecator
33
- ActiveSupport.deprecator.warn("assert_deprecated without a deprecator is deprecated")
34
- deprecator = ActiveSupport::Deprecation._instance
34
+ raise ArgumentError, "No deprecator given"
35
35
  end
36
+
36
37
  result, warnings = collect_deprecations(deprecator, &block)
37
38
  assert !warnings.empty?, "Expected a deprecation warning within the block but received none"
38
39
  if match
@@ -51,11 +52,7 @@ module ActiveSupport
51
52
  # assert_not_deprecated(ActiveSupport::Deprecation.new) do
52
53
  # CustomDeprecator.warn "message" # passes assertion, different deprecator
53
54
  # end
54
- def assert_not_deprecated(deprecator = nil, &block)
55
- unless deprecator
56
- ActiveSupport.deprecator.warn("assert_not_deprecated without a deprecator is deprecated")
57
- deprecator = ActiveSupport::Deprecation._instance
58
- end
55
+ def assert_not_deprecated(deprecator, &block)
59
56
  result, deprecations = collect_deprecations(deprecator, &block)
60
57
  assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}"
61
58
  result
@@ -69,11 +66,7 @@ module ActiveSupport
69
66
  # ActiveSupport::Deprecation.new.warn "other message"
70
67
  # :result
71
68
  # end # => [:result, ["message"]]
72
- def collect_deprecations(deprecator = nil)
73
- unless deprecator
74
- ActiveSupport.deprecator.warn("collect_deprecations without a deprecator is deprecated")
75
- deprecator = ActiveSupport::Deprecation._instance
76
- end
69
+ def collect_deprecations(deprecator)
77
70
  old_behavior = deprecator.behavior
78
71
  deprecations = []
79
72
  deprecator.behavior = Proc.new do |message, callstack|
@@ -44,7 +44,7 @@ module ActiveSupport
44
44
  ActiveSupport.error_reporter.subscribe(self)
45
45
  @subscribed = true
46
46
  else
47
- raise Minitest::Assertion, "No error reporter is configured"
47
+ flunk("No error reporter is configured")
48
48
  end
49
49
  end
50
50
  end
@@ -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
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/testing/parallelize_executor"
4
+
3
5
  module ActiveSupport
4
6
  module Testing
5
7
  module Isolation
6
- require "thread"
8
+ SubprocessCrashed = Class.new(StandardError)
7
9
 
8
10
  def self.included(klass) # :nodoc:
9
11
  klass.class_eval do
10
- parallelize_me!
12
+ parallelize_me! unless Minitest.parallel_executor.is_a?(ActiveSupport::Testing::ParallelizeExecutor)
11
13
  end
12
14
  end
13
15
 
@@ -16,10 +18,17 @@ module ActiveSupport
16
18
  end
17
19
 
18
20
  def run
19
- serialized = run_in_isolation do
21
+ status, serialized = run_in_isolation do
20
22
  super
21
23
  end
22
24
 
25
+ unless status&.success?
26
+ error = SubprocessCrashed.new("Subprocess exited with an error: #{status.inspect}\noutput: #{serialized.inspect}")
27
+ error.set_backtrace(caller)
28
+ self.failures << Minitest::UnexpectedError.new(error)
29
+ return defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
30
+ end
31
+
23
32
  Marshal.load(serialized)
24
33
  end
25
34
 
@@ -50,13 +59,13 @@ module ActiveSupport
50
59
  end
51
60
 
52
61
  write.puts [result].pack("m")
53
- exit!
62
+ exit!(0)
54
63
  end
55
64
 
56
65
  write.close
57
66
  result = read.read
58
- Process.wait2(pid)
59
- result.unpack1("m")
67
+ _, status = Process.wait2(pid)
68
+ return status, result.unpack1("m")
60
69
  end
61
70
  end
62
71
  end
@@ -75,7 +84,7 @@ module ActiveSupport
75
84
  File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
76
85
  file.puts [Marshal.dump(test_result)].pack("m")
77
86
  end
78
- exit!
87
+ exit!(0)
79
88
  else
80
89
  Tempfile.open("isolation") do |tmpfile|
81
90
  env = {
@@ -93,13 +102,14 @@ module ActiveSupport
93
102
 
94
103
  child = IO.popen([env, Gem.ruby, *load_path_args, $0, *ORIG_ARGV, test_opts])
95
104
 
105
+ status = nil
96
106
  begin
97
- Process.wait(child.pid)
107
+ _, status = Process.wait2(child.pid)
98
108
  rescue Errno::ECHILD # The child process may exit before we wait
99
109
  nil
100
110
  end
101
111
 
102
- return tmpfile.read.unpack1("m")
112
+ return status, tmpfile.read.unpack1("m")
103
113
  end
104
114
  end
105
115
  end
@@ -30,22 +30,8 @@ module ActiveSupport
30
30
  assert_called(object, method_name, message, times: 0, &block)
31
31
  end
32
32
 
33
- #--
34
- # This method is a temporary wrapper for mock.expect as part of
35
- # the Minitest 5.16 / Ruby 3.0 kwargs transition. It can go away
36
- # when we drop support for Ruby 2.7.
37
- if Minitest::Mock.instance_method(:expect).parameters.map(&:first).include?(:keyrest)
38
- def expect_called_with(mock, args, returns: false, **kwargs)
39
- mock.expect(:call, returns, args, **kwargs)
40
- end
41
- else
42
- def expect_called_with(mock, args, returns: false, **kwargs)
43
- if !kwargs.empty?
44
- mock.expect(:call, returns, [*args, kwargs])
45
- else
46
- mock.expect(:call, returns, args)
47
- end
48
- end
33
+ def expect_called_with(mock, args, returns: false, **kwargs)
34
+ mock.expect(:call, returns, args, **kwargs)
49
35
  end
50
36
 
51
37
  def assert_called_on_instance_of(klass, method_name, message = nil, times: 1, returns: nil)