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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +422 -145
- data/README.rdoc +1 -1
- data/lib/active_support/backtrace_cleaner.rb +73 -2
- data/lib/active_support/benchmark.rb +21 -0
- data/lib/active_support/benchmarkable.rb +3 -2
- data/lib/active_support/broadcast_logger.rb +61 -74
- data/lib/active_support/cache/file_store.rb +14 -4
- data/lib/active_support/cache/mem_cache_store.rb +30 -29
- data/lib/active_support/cache/memory_store.rb +11 -5
- data/lib/active_support/cache/null_store.rb +2 -2
- data/lib/active_support/cache/redis_cache_store.rb +43 -34
- data/lib/active_support/cache/strategy/local_cache.rb +72 -27
- data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
- data/lib/active_support/cache.rb +88 -20
- data/lib/active_support/callbacks.rb +28 -13
- data/lib/active_support/class_attribute.rb +33 -0
- data/lib/active_support/code_generator.rb +9 -0
- data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
- data/lib/active_support/concurrency/share_lock.rb +0 -1
- data/lib/active_support/concurrency/thread_monitor.rb +55 -0
- data/lib/active_support/configurable.rb +34 -0
- data/lib/active_support/configuration_file.rb +15 -6
- data/lib/active_support/continuous_integration.rb +145 -0
- data/lib/active_support/core_ext/array/conversions.rb +3 -3
- data/lib/active_support/core_ext/array.rb +7 -7
- data/lib/active_support/core_ext/benchmark.rb +0 -15
- data/lib/active_support/core_ext/big_decimal.rb +1 -1
- data/lib/active_support/core_ext/class/attribute.rb +26 -20
- data/lib/active_support/core_ext/class.rb +2 -2
- data/lib/active_support/core_ext/date/conversions.rb +2 -0
- data/lib/active_support/core_ext/date.rb +5 -5
- data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -35
- data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
- data/lib/active_support/core_ext/date_time/conversions.rb +4 -2
- data/lib/active_support/core_ext/date_time.rb +5 -5
- data/lib/active_support/core_ext/digest.rb +1 -1
- data/lib/active_support/core_ext/enumerable.rb +25 -8
- data/lib/active_support/core_ext/erb/util.rb +5 -5
- data/lib/active_support/core_ext/file.rb +1 -1
- data/lib/active_support/core_ext/hash/deep_merge.rb +1 -0
- data/lib/active_support/core_ext/hash/except.rb +0 -12
- data/lib/active_support/core_ext/hash.rb +8 -8
- data/lib/active_support/core_ext/integer.rb +3 -3
- data/lib/active_support/core_ext/kernel.rb +3 -3
- data/lib/active_support/core_ext/module/attr_internal.rb +3 -4
- data/lib/active_support/core_ext/module/introspection.rb +3 -0
- data/lib/active_support/core_ext/module.rb +11 -11
- data/lib/active_support/core_ext/numeric.rb +3 -3
- data/lib/active_support/core_ext/object/json.rb +24 -11
- data/lib/active_support/core_ext/object/to_query.rb +7 -1
- data/lib/active_support/core_ext/object/try.rb +2 -2
- data/lib/active_support/core_ext/object.rb +13 -13
- data/lib/active_support/core_ext/pathname.rb +2 -2
- data/lib/active_support/core_ext/range/overlap.rb +3 -3
- data/lib/active_support/core_ext/range/sole.rb +17 -0
- data/lib/active_support/core_ext/range.rb +4 -4
- data/lib/active_support/core_ext/securerandom.rb +24 -8
- data/lib/active_support/core_ext/string/filters.rb +3 -3
- data/lib/active_support/core_ext/string/inflections.rb +1 -1
- data/lib/active_support/core_ext/string/multibyte.rb +12 -3
- data/lib/active_support/core_ext/string/output_safety.rb +29 -13
- data/lib/active_support/core_ext/string.rb +13 -13
- data/lib/active_support/core_ext/symbol.rb +1 -1
- data/lib/active_support/core_ext/thread/backtrace/location.rb +2 -7
- data/lib/active_support/core_ext/time/calculations.rb +7 -2
- data/lib/active_support/core_ext/time/compatibility.rb +2 -19
- data/lib/active_support/core_ext/time/conversions.rb +2 -0
- data/lib/active_support/core_ext/time.rb +5 -5
- data/lib/active_support/current_attributes/test_helper.rb +2 -2
- data/lib/active_support/current_attributes.rb +27 -17
- data/lib/active_support/delegation.rb +25 -44
- data/lib/active_support/dependencies/interlock.rb +11 -5
- data/lib/active_support/dependencies.rb +6 -2
- data/lib/active_support/deprecation/reporting.rb +4 -21
- data/lib/active_support/deprecation.rb +1 -1
- data/lib/active_support/duration.rb +14 -10
- data/lib/active_support/editor.rb +70 -0
- data/lib/active_support/encrypted_configuration.rb +20 -2
- data/lib/active_support/error_reporter.rb +81 -4
- data/lib/active_support/event_reporter/test_helper.rb +32 -0
- data/lib/active_support/event_reporter.rb +592 -0
- data/lib/active_support/evented_file_update_checker.rb +5 -2
- data/lib/active_support/execution_context.rb +75 -7
- data/lib/active_support/execution_wrapper.rb +1 -1
- data/lib/active_support/file_update_checker.rb +8 -6
- data/lib/active_support/gem_version.rb +4 -4
- data/lib/active_support/gzip.rb +1 -0
- data/lib/active_support/hash_with_indifferent_access.rb +61 -38
- data/lib/active_support/i18n_railtie.rb +19 -11
- data/lib/active_support/inflector/inflections.rb +34 -16
- data/lib/active_support/inflector/methods.rb +3 -3
- data/lib/active_support/inflector/transliterate.rb +6 -8
- data/lib/active_support/isolated_execution_state.rb +17 -17
- data/lib/active_support/json/decoding.rb +6 -4
- data/lib/active_support/json/encoding.rb +159 -21
- data/lib/active_support/lazy_load_hooks.rb +1 -1
- data/lib/active_support/log_subscriber.rb +2 -6
- data/lib/active_support/logger_thread_safe_level.rb +6 -3
- data/lib/active_support/message_encryptors.rb +54 -2
- data/lib/active_support/message_pack/extensions.rb +6 -1
- data/lib/active_support/message_verifier.rb +9 -0
- data/lib/active_support/message_verifiers.rb +57 -3
- data/lib/active_support/messages/rotation_coordinator.rb +9 -0
- data/lib/active_support/messages/rotator.rb +10 -0
- data/lib/active_support/multibyte/chars.rb +12 -2
- data/lib/active_support/multibyte.rb +4 -0
- data/lib/active_support/notifications/fanout.rb +64 -43
- data/lib/active_support/notifications/instrumenter.rb +1 -1
- data/lib/active_support/number_helper/number_converter.rb +1 -1
- data/lib/active_support/number_helper/number_to_delimited_converter.rb +17 -2
- data/lib/active_support/number_helper.rb +22 -0
- data/lib/active_support/railtie.rb +32 -9
- data/lib/active_support/structured_event_subscriber.rb +99 -0
- data/lib/active_support/subscriber.rb +0 -5
- data/lib/active_support/syntax_error_proxy.rb +7 -0
- data/lib/active_support/tagged_logging.rb +5 -0
- data/lib/active_support/test_case.rb +67 -6
- data/lib/active_support/testing/assertions.rb +118 -27
- data/lib/active_support/testing/autorun.rb +5 -0
- data/lib/active_support/testing/error_reporter_assertions.rb +17 -0
- data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
- data/lib/active_support/testing/isolation.rb +0 -2
- data/lib/active_support/testing/notification_assertions.rb +92 -0
- data/lib/active_support/testing/parallelization/server.rb +15 -2
- data/lib/active_support/testing/parallelization/worker.rb +9 -3
- data/lib/active_support/testing/parallelization.rb +25 -1
- data/lib/active_support/testing/tests_without_assertions.rb +1 -1
- data/lib/active_support/testing/time_helpers.rb +9 -4
- data/lib/active_support/time_with_zone.rb +36 -23
- data/lib/active_support/values/time_zone.rb +19 -10
- data/lib/active_support/xml_mini.rb +3 -2
- data/lib/active_support.rb +21 -9
- metadata +35 -16
- data/lib/active_support/core_ext/range/each.rb +0 -24
- data/lib/active_support/proxy_object.rb +0 -20
- 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
|
|
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
|
|
80
|
+
# A hash of expressions/numeric differences can be passed in and evaluated.
|
|
81
81
|
#
|
|
82
|
-
# assert_difference
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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,
|
|
307
|
+
assert_nil after, rich_message
|
|
257
308
|
else
|
|
258
|
-
assert_equal before, after,
|
|
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
|
|
@@ -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
|
|
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
|