activesupport 8.0.3 → 8.1.0.beta1
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 +237 -175
- data/lib/active_support/backtrace_cleaner.rb +71 -0
- data/lib/active_support/cache/mem_cache_store.rb +13 -13
- data/lib/active_support/cache/redis_cache_store.rb +36 -30
- data/lib/active_support/cache/strategy/local_cache.rb +16 -7
- data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
- data/lib/active_support/cache.rb +69 -6
- data/lib/active_support/configurable.rb +28 -0
- data/lib/active_support/continuous_integration.rb +145 -0
- data/lib/active_support/core_ext/benchmark.rb +0 -1
- data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
- data/lib/active_support/core_ext/erb/util.rb +3 -3
- data/lib/active_support/core_ext/object/json.rb +8 -1
- data/lib/active_support/core_ext/object/to_query.rb +5 -0
- data/lib/active_support/core_ext/range.rb +0 -1
- data/lib/active_support/core_ext/string/multibyte.rb +10 -1
- data/lib/active_support/core_ext/string/output_safety.rb +19 -12
- data/lib/active_support/current_attributes/test_helper.rb +2 -2
- data/lib/active_support/current_attributes.rb +13 -10
- data/lib/active_support/deprecation/reporting.rb +4 -2
- data/lib/active_support/deprecation.rb +1 -1
- data/lib/active_support/editor.rb +70 -0
- data/lib/active_support/error_reporter.rb +50 -6
- data/lib/active_support/event_reporter/test_helper.rb +32 -0
- data/lib/active_support/event_reporter.rb +570 -0
- data/lib/active_support/evented_file_update_checker.rb +5 -1
- data/lib/active_support/execution_context.rb +64 -7
- data/lib/active_support/file_update_checker.rb +8 -6
- data/lib/active_support/gem_version.rb +3 -3
- data/lib/active_support/gzip.rb +1 -0
- data/lib/active_support/hash_with_indifferent_access.rb +27 -7
- data/lib/active_support/i18n_railtie.rb +1 -2
- data/lib/active_support/inflector/inflections.rb +31 -15
- data/lib/active_support/inflector/transliterate.rb +6 -8
- data/lib/active_support/isolated_execution_state.rb +7 -13
- data/lib/active_support/json/decoding.rb +2 -2
- data/lib/active_support/json/encoding.rb +103 -14
- data/lib/active_support/log_subscriber.rb +2 -0
- data/lib/active_support/message_encryptors.rb +52 -0
- data/lib/active_support/message_pack/extensions.rb +5 -0
- data/lib/active_support/message_verifiers.rb +52 -0
- data/lib/active_support/messages/rotation_coordinator.rb +9 -0
- data/lib/active_support/messages/rotator.rb +5 -0
- data/lib/active_support/multibyte/chars.rb +8 -1
- data/lib/active_support/multibyte.rb +4 -0
- data/lib/active_support/railtie.rb +26 -12
- data/lib/active_support/syntax_error_proxy.rb +3 -0
- data/lib/active_support/test_case.rb +61 -6
- data/lib/active_support/testing/assertions.rb +34 -6
- data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
- data/lib/active_support/testing/event_reporter_assertions.rb +217 -0
- data/lib/active_support/testing/notification_assertions.rb +92 -0
- data/lib/active_support/testing/parallelization/worker.rb +2 -0
- data/lib/active_support/testing/parallelization.rb +13 -0
- data/lib/active_support/testing/tests_without_assertions.rb +1 -1
- data/lib/active_support/testing/time_helpers.rb +7 -3
- data/lib/active_support/time_with_zone.rb +19 -5
- data/lib/active_support/values/time_zone.rb +8 -1
- data/lib/active_support/xml_mini.rb +1 -2
- data/lib/active_support.rb +11 -0
- metadata +10 -5
- data/lib/active_support/core_ext/range/each.rb +0 -24
@@ -26,6 +26,9 @@ module ActiveSupport
|
|
26
26
|
# as the first rotation and <tt>transitional = true</tt>. Then, after all
|
27
27
|
# servers have been updated, perform a second rolling deploy with
|
28
28
|
# <tt>transitional = false</tt>.
|
29
|
+
#
|
30
|
+
#--
|
31
|
+
# Implemented by ActiveSupport::Messages::RotationCoordinator#transitional
|
29
32
|
|
30
33
|
##
|
31
34
|
# :singleton-method: new
|
@@ -42,6 +45,9 @@ module ActiveSupport
|
|
42
45
|
# end
|
43
46
|
#
|
44
47
|
# verifiers.rotate(base: "...")
|
48
|
+
#
|
49
|
+
#--
|
50
|
+
# Implemented by ActiveSupport::Messages::RotationCoordinator#initialize
|
45
51
|
|
46
52
|
##
|
47
53
|
# :method: []
|
@@ -50,12 +56,18 @@ module ActiveSupport
|
|
50
56
|
# Returns a MessageVerifier configured with a secret derived from the
|
51
57
|
# given +salt+, and options from #rotate. MessageVerifier instances will
|
52
58
|
# be memoized, so the same +salt+ will return the same instance.
|
59
|
+
#
|
60
|
+
#--
|
61
|
+
# Implemented by ActiveSupport::Messages::RotationCoordinator#[]
|
53
62
|
|
54
63
|
##
|
55
64
|
# :method: []=
|
56
65
|
# :call-seq: []=(salt, verifier)
|
57
66
|
#
|
58
67
|
# Overrides a MessageVerifier instance associated with a given +salt+.
|
68
|
+
#
|
69
|
+
#--
|
70
|
+
# Implemented by ActiveSupport::Messages::RotationCoordinator#[]=
|
59
71
|
|
60
72
|
##
|
61
73
|
# :method: rotate
|
@@ -104,18 +116,55 @@ module ActiveSupport
|
|
104
116
|
#
|
105
117
|
# # Uses `serializer: Marshal, url_safe: false`.
|
106
118
|
# verifiers[:baz]
|
119
|
+
#
|
120
|
+
#--
|
121
|
+
# Implemented by ActiveSupport::Messages::RotationCoordinator#rotate
|
122
|
+
|
123
|
+
##
|
124
|
+
# :method: prepend
|
125
|
+
# :call-seq:
|
126
|
+
# prepend(**options)
|
127
|
+
# prepend(&block)
|
128
|
+
#
|
129
|
+
# Just like #rotate, but prepends the given options or block to the list of
|
130
|
+
# option sets.
|
131
|
+
#
|
132
|
+
# This can be useful when you have an already-configured +MessageVerifiers+
|
133
|
+
# instance, but you want to override the way messages are signed.
|
134
|
+
#
|
135
|
+
# module ThirdParty
|
136
|
+
# VERIFIERS = ActiveSupport::MessageVerifiers.new { ... }.
|
137
|
+
# rotate(serializer: Marshal, url_safe: true).
|
138
|
+
# rotate(serializer: Marshal, url_safe: false)
|
139
|
+
# end
|
140
|
+
#
|
141
|
+
# ThirdParty.VERIFIERS.prepend(serializer: JSON, url_safe: true)
|
142
|
+
#
|
143
|
+
# # Uses `serializer: JSON, url_safe: true`.
|
144
|
+
# # Falls back to `serializer: Marshal, url_safe: true` or
|
145
|
+
# # `serializer: Marshal, url_safe: false`.
|
146
|
+
# ThirdParty.VERIFIERS[:foo]
|
147
|
+
#
|
148
|
+
#--
|
149
|
+
# Implemented by ActiveSupport::Messages::RotationCoordinator#prepend
|
107
150
|
|
108
151
|
##
|
109
152
|
# :method: rotate_defaults
|
110
153
|
# :call-seq: rotate_defaults
|
111
154
|
#
|
112
155
|
# Invokes #rotate with the default options.
|
156
|
+
#
|
157
|
+
#--
|
158
|
+
# Implemented by ActiveSupport::Messages::RotationCoordinator#rotate_defaults
|
113
159
|
|
114
160
|
##
|
115
161
|
# :method: clear_rotations
|
116
162
|
# :call-seq: clear_rotations
|
117
163
|
#
|
118
164
|
# Clears the list of option sets.
|
165
|
+
#
|
166
|
+
#--
|
167
|
+
# Implemented by ActiveSupport::Messages::RotationCoordinator#clear_rotations
|
119
168
|
|
120
169
|
##
|
121
170
|
# :method: on_rotation
|
@@ -127,6 +176,9 @@ module ActiveSupport
|
|
127
176
|
# For example, this callback could log each time it is called, and thus
|
128
177
|
# indicate whether old option sets are still in use or can be removed from
|
129
178
|
# rotation.
|
179
|
+
#
|
180
|
+
#--
|
181
|
+
# Implemented by ActiveSupport::Messages::RotationCoordinator#on_rotation
|
130
182
|
|
131
183
|
##
|
132
184
|
private
|
@@ -32,6 +32,15 @@ module ActiveSupport
|
|
32
32
|
self
|
33
33
|
end
|
34
34
|
|
35
|
+
def prepend(**options, &block)
|
36
|
+
raise ArgumentError, "Options cannot be specified when using a block" if block && !options.empty?
|
37
|
+
changing_configuration!
|
38
|
+
|
39
|
+
@rotate_options.unshift(block || options)
|
40
|
+
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
35
44
|
def rotate_defaults
|
36
45
|
rotate()
|
37
46
|
end
|
@@ -53,7 +53,14 @@ module ActiveSupport # :nodoc:
|
|
53
53
|
delegate :<=>, :=~, :match?, :acts_like_string?, to: :wrapped_string
|
54
54
|
|
55
55
|
# Creates a new Chars instance by wrapping _string_.
|
56
|
-
def initialize(string)
|
56
|
+
def initialize(string, deprecation: true)
|
57
|
+
if deprecation
|
58
|
+
ActiveSupport.deprecator.warn(
|
59
|
+
"ActiveSupport::Multibyte::Chars is deprecated and will be removed in Rails 8.2. " \
|
60
|
+
"Use normal string methods instead."
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
57
64
|
@wrapped_string = string
|
58
65
|
if string.encoding != Encoding::UTF_8
|
59
66
|
@wrapped_string = @wrapped_string.dup
|
@@ -12,6 +12,10 @@ module ActiveSupport # :nodoc:
|
|
12
12
|
#
|
13
13
|
# ActiveSupport::Multibyte.proxy_class = CharsForUTF32
|
14
14
|
def self.proxy_class=(klass)
|
15
|
+
ActiveSupport.deprecator.warn(
|
16
|
+
"ActiveSupport::Multibyte.proxy_class= is deprecated and will be removed in Rails 8.2. " \
|
17
|
+
"Use normal string methods instead."
|
18
|
+
)
|
15
19
|
@proxy_class = klass
|
16
20
|
end
|
17
21
|
|
@@ -15,7 +15,7 @@ module ActiveSupport
|
|
15
15
|
|
16
16
|
initializer "active_support.isolation_level" do |app|
|
17
17
|
config.after_initialize do
|
18
|
-
if level = app.config.active_support.
|
18
|
+
if level = app.config.active_support.isolation_level
|
19
19
|
ActiveSupport::IsolatedExecutionState.isolation_level = level
|
20
20
|
end
|
21
21
|
end
|
@@ -38,19 +38,35 @@ module ActiveSupport
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
initializer "active_support.
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
initializer "active_support.set_event_reporter_context_store" do |app|
|
42
|
+
config.after_initialize do
|
43
|
+
if klass = app.config.active_support.event_reporter_context_store
|
44
|
+
ActiveSupport::EventReporter.context_store = klass
|
45
|
+
end
|
46
|
+
end
|
45
47
|
end
|
46
48
|
|
47
|
-
initializer "active_support.
|
48
|
-
app.reloader.before_class_unload
|
49
|
-
|
50
|
-
|
49
|
+
initializer "active_support.reset_execution_context" do |app|
|
50
|
+
app.reloader.before_class_unload do
|
51
|
+
ActiveSupport::CurrentAttributes.clear_all
|
52
|
+
ActiveSupport::ExecutionContext.clear
|
53
|
+
ActiveSupport.event_reporter.clear_context
|
54
|
+
end
|
55
|
+
|
56
|
+
app.executor.to_run do
|
57
|
+
ActiveSupport::ExecutionContext.push
|
58
|
+
end
|
59
|
+
|
60
|
+
app.executor.to_complete do
|
61
|
+
ActiveSupport::CurrentAttributes.clear_all
|
62
|
+
ActiveSupport::ExecutionContext.pop
|
63
|
+
ActiveSupport.event_reporter.clear_context
|
64
|
+
end
|
51
65
|
|
52
66
|
ActiveSupport.on_load(:active_support_test_case) do
|
53
67
|
if app.config.active_support.executor_around_test_case
|
68
|
+
ActiveSupport::ExecutionContext.nestable = true
|
69
|
+
|
54
70
|
require "active_support/executor/test_helper"
|
55
71
|
include ActiveSupport::Executor::TestHelper
|
56
72
|
else
|
@@ -97,9 +113,7 @@ module ActiveSupport
|
|
97
113
|
end
|
98
114
|
|
99
115
|
initializer "active_support.to_time_preserves_timezone" do |app|
|
100
|
-
config.
|
101
|
-
ActiveSupport.to_time_preserves_timezone = app.config.active_support.to_time_preserves_timezone
|
102
|
-
end
|
116
|
+
ActiveSupport.to_time_preserves_timezone = app.config.active_support.to_time_preserves_timezone
|
103
117
|
end
|
104
118
|
|
105
119
|
# Sets the default week start
|
@@ -6,6 +6,7 @@ require "active_support/testing/setup_and_teardown"
|
|
6
6
|
require "active_support/testing/tests_without_assertions"
|
7
7
|
require "active_support/testing/assertions"
|
8
8
|
require "active_support/testing/error_reporter_assertions"
|
9
|
+
require "active_support/testing/event_reporter_assertions"
|
9
10
|
require "active_support/testing/deprecation"
|
10
11
|
require "active_support/testing/declarative"
|
11
12
|
require "active_support/testing/isolation"
|
@@ -15,13 +16,29 @@ require "active_support/testing/constant_stubbing"
|
|
15
16
|
require "active_support/testing/file_fixtures"
|
16
17
|
require "active_support/testing/parallelization"
|
17
18
|
require "active_support/testing/parallelize_executor"
|
19
|
+
require "active_support/testing/notification_assertions"
|
18
20
|
require "concurrent/utility/processor_counter"
|
19
21
|
|
20
22
|
module ActiveSupport
|
21
23
|
class TestCase < ::Minitest::Test
|
22
24
|
Assertion = Minitest::Assertion
|
23
25
|
|
26
|
+
# Class variable to store the parallel worker ID
|
27
|
+
@@parallel_worker_id = nil
|
28
|
+
|
24
29
|
class << self
|
30
|
+
# Returns the current parallel worker ID if tests are running in parallel,
|
31
|
+
# nil otherwise.
|
32
|
+
#
|
33
|
+
# ActiveSupport::TestCase.parallel_worker_id # => 2
|
34
|
+
def parallel_worker_id
|
35
|
+
@@parallel_worker_id
|
36
|
+
end
|
37
|
+
|
38
|
+
def parallel_worker_id=(value) # :nodoc:
|
39
|
+
@@parallel_worker_id = value
|
40
|
+
end
|
41
|
+
|
25
42
|
# Sets the order in which test cases are run.
|
26
43
|
#
|
27
44
|
# ActiveSupport::TestCase.test_order = :random # => :random
|
@@ -51,8 +68,8 @@ module ActiveSupport
|
|
51
68
|
# is forked. For each process a new database will be created suffixed
|
52
69
|
# with the worker number.
|
53
70
|
#
|
54
|
-
# test-
|
55
|
-
# test-
|
71
|
+
# test-database_0
|
72
|
+
# test-database_1
|
56
73
|
#
|
57
74
|
# If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored
|
58
75
|
# and the environment variable will be used instead. This is useful for CI
|
@@ -78,14 +95,45 @@ module ActiveSupport
|
|
78
95
|
# Because parallelization presents an overhead, it is only enabled when the
|
79
96
|
# number of tests to run is above the +threshold+ param. The default value is
|
80
97
|
# 50, and it's configurable via +config.active_support.test_parallelization_threshold+.
|
81
|
-
|
82
|
-
|
83
|
-
|
98
|
+
#
|
99
|
+
# If you want to skip Rails default creation of one database per process in favor of
|
100
|
+
# writing your own implementation, you can set +parallelize_databases+, or configure it
|
101
|
+
# via +config.active_support.parallelize_test_databases+.
|
102
|
+
#
|
103
|
+
# parallelize(workers: :number_of_processors, parallelize_databases: false)
|
104
|
+
#
|
105
|
+
# Note that your test suite may deadlock if you attempt to use only one database
|
106
|
+
# with multiple processes.
|
107
|
+
def parallelize(workers: :number_of_processors, with: :processes, threshold: ActiveSupport.test_parallelization_threshold, parallelize_databases: ActiveSupport.parallelize_test_databases)
|
108
|
+
case
|
109
|
+
when ENV["PARALLEL_WORKERS"]
|
110
|
+
workers = ENV["PARALLEL_WORKERS"].to_i
|
111
|
+
when workers == :number_of_processors
|
112
|
+
workers = (Concurrent.available_processor_count || Concurrent.processor_count).floor
|
113
|
+
end
|
114
|
+
|
115
|
+
if with == :processes
|
116
|
+
ActiveSupport.parallelize_test_databases = parallelize_databases
|
117
|
+
end
|
84
118
|
|
85
119
|
Minitest.parallel_executor = ActiveSupport::Testing::ParallelizeExecutor.new(size: workers, with: with, threshold: threshold)
|
86
120
|
end
|
87
121
|
|
88
|
-
#
|
122
|
+
# Before fork hook for parallel testing. This can be used to run anything
|
123
|
+
# before the processes are forked.
|
124
|
+
#
|
125
|
+
# In your +test_helper.rb+ add the following:
|
126
|
+
#
|
127
|
+
# class ActiveSupport::TestCase
|
128
|
+
# parallelize_before_fork do
|
129
|
+
# # run this before fork
|
130
|
+
# end
|
131
|
+
# end
|
132
|
+
def parallelize_before_fork(&block)
|
133
|
+
ActiveSupport::Testing::Parallelization.before_fork_hook(&block)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Setup hook for parallel testing. This can be used if you have multiple
|
89
137
|
# databases or any behavior that needs to be run after the process is forked
|
90
138
|
# but before the tests run.
|
91
139
|
#
|
@@ -141,11 +189,18 @@ module ActiveSupport
|
|
141
189
|
|
142
190
|
alias_method :method_name, :name
|
143
191
|
|
192
|
+
# Returns the current parallel worker ID if tests are running in parallel
|
193
|
+
def parallel_worker_id
|
194
|
+
self.class.parallel_worker_id
|
195
|
+
end
|
196
|
+
|
144
197
|
include ActiveSupport::Testing::TaggedLogging
|
145
198
|
prepend ActiveSupport::Testing::SetupAndTeardown
|
146
199
|
prepend ActiveSupport::Testing::TestsWithoutAssertions
|
147
200
|
include ActiveSupport::Testing::Assertions
|
148
201
|
include ActiveSupport::Testing::ErrorReporterAssertions
|
202
|
+
include ActiveSupport::Testing::EventReporterAssertions
|
203
|
+
include ActiveSupport::Testing::NotificationAssertions
|
149
204
|
include ActiveSupport::Testing::Deprecation
|
150
205
|
include ActiveSupport::Testing::ConstantStubbing
|
151
206
|
include ActiveSupport::Testing::TimeHelpers
|
@@ -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
|
@@ -181,12 +185,24 @@ module ActiveSupport
|
|
181
185
|
#
|
182
186
|
# The keyword arguments +:from+ and +:to+ can be given to specify the
|
183
187
|
# expected initial value and the expected value after the block was
|
184
|
-
# executed.
|
188
|
+
# executed. The comparison is done using case equality (===), which means
|
189
|
+
# you can specify patterns or classes:
|
185
190
|
#
|
191
|
+
# # Exact value match
|
186
192
|
# assert_changes :@object, from: nil, to: :foo do
|
187
193
|
# @object = :foo
|
188
194
|
# end
|
189
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
|
+
#
|
190
206
|
# An error message can be specified.
|
191
207
|
#
|
192
208
|
# assert_changes -> { Status.all_good? }, 'Expected the status to be bad' do
|
@@ -238,12 +254,24 @@ module ActiveSupport
|
|
238
254
|
# end
|
239
255
|
#
|
240
256
|
# Provide the optional keyword argument +:from+ to specify the expected
|
241
|
-
# initial value.
|
257
|
+
# initial value. The comparison is done using case equality (===), which means
|
258
|
+
# you can specify patterns or classes:
|
242
259
|
#
|
260
|
+
# # Exact value match
|
243
261
|
# assert_no_changes -> { Status.all_good? }, from: true do
|
244
262
|
# post :create, params: { status: { ok: true } }
|
245
263
|
# end
|
246
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
|
+
#
|
247
275
|
# An error message can be specified.
|
248
276
|
#
|
249
277
|
# assert_no_changes -> { Status.all_good? }, 'Expected the status to be good' do
|
@@ -44,7 +44,7 @@ module ActiveSupport
|
|
44
44
|
ActiveSupport.error_reporter.subscribe(self)
|
45
45
|
@subscribed = true
|
46
46
|
else
|
47
|
-
|
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,217 @@
|
|
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}\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
|
+
end
|
216
|
+
end
|
217
|
+
end
|