activesupport 8.0.5.2 → 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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +220 -278
  3. data/lib/active_support/backtrace_cleaner.rb +71 -0
  4. data/lib/active_support/cache/mem_cache_store.rb +14 -17
  5. data/lib/active_support/cache/redis_cache_store.rb +37 -31
  6. data/lib/active_support/cache/strategy/local_cache.rb +16 -7
  7. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  8. data/lib/active_support/cache.rb +69 -6
  9. data/lib/active_support/configurable.rb +28 -0
  10. data/lib/active_support/continuous_integration.rb +145 -0
  11. data/lib/active_support/core_ext/benchmark.rb +0 -1
  12. data/lib/active_support/core_ext/class/attribute.rb +6 -8
  13. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  14. data/lib/active_support/core_ext/enumerable.rb +2 -2
  15. data/lib/active_support/core_ext/erb/util.rb +3 -3
  16. data/lib/active_support/core_ext/object/json.rb +9 -2
  17. data/lib/active_support/core_ext/object/to_query.rb +5 -0
  18. data/lib/active_support/core_ext/range.rb +0 -1
  19. data/lib/active_support/core_ext/string/inflections.rb +1 -1
  20. data/lib/active_support/core_ext/string/multibyte.rb +10 -1
  21. data/lib/active_support/core_ext/string/output_safety.rb +20 -15
  22. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  23. data/lib/active_support/current_attributes.rb +13 -10
  24. data/lib/active_support/delegation.rb +42 -25
  25. data/lib/active_support/deprecation/reporting.rb +4 -2
  26. data/lib/active_support/deprecation.rb +1 -1
  27. data/lib/active_support/editor.rb +70 -0
  28. data/lib/active_support/error_reporter.rb +50 -6
  29. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  30. data/lib/active_support/event_reporter.rb +570 -0
  31. data/lib/active_support/evented_file_update_checker.rb +5 -1
  32. data/lib/active_support/execution_context.rb +64 -7
  33. data/lib/active_support/file_update_checker.rb +8 -6
  34. data/lib/active_support/gem_version.rb +3 -3
  35. data/lib/active_support/gzip.rb +1 -0
  36. data/lib/active_support/hash_with_indifferent_access.rb +27 -7
  37. data/lib/active_support/i18n_railtie.rb +1 -2
  38. data/lib/active_support/inflector/inflections.rb +32 -17
  39. data/lib/active_support/inflector/methods.rb +21 -5
  40. data/lib/active_support/inflector/transliterate.rb +6 -8
  41. data/lib/active_support/isolated_execution_state.rb +9 -17
  42. data/lib/active_support/json/decoding.rb +2 -2
  43. data/lib/active_support/json/encoding.rb +103 -14
  44. data/lib/active_support/log_subscriber.rb +2 -0
  45. data/lib/active_support/message_encryptors.rb +52 -0
  46. data/lib/active_support/message_pack/extensions.rb +6 -1
  47. data/lib/active_support/message_verifiers.rb +52 -0
  48. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  49. data/lib/active_support/messages/rotator.rb +5 -0
  50. data/lib/active_support/multibyte/chars.rb +8 -1
  51. data/lib/active_support/multibyte.rb +4 -0
  52. data/lib/active_support/notifications/instrumenter.rb +1 -1
  53. data/lib/active_support/number_helper/number_converter.rb +1 -1
  54. data/lib/active_support/number_helper/number_to_delimited_converter.rb +2 -17
  55. data/lib/active_support/railtie.rb +26 -12
  56. data/lib/active_support/syntax_error_proxy.rb +3 -0
  57. data/lib/active_support/test_case.rb +61 -12
  58. data/lib/active_support/testing/assertions.rb +37 -14
  59. data/lib/active_support/testing/autorun.rb +0 -5
  60. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  61. data/lib/active_support/testing/event_reporter_assertions.rb +217 -0
  62. data/lib/active_support/testing/notification_assertions.rb +92 -0
  63. data/lib/active_support/testing/parallelization/server.rb +2 -15
  64. data/lib/active_support/testing/parallelization/worker.rb +5 -7
  65. data/lib/active_support/testing/parallelization.rb +14 -12
  66. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  67. data/lib/active_support/testing/time_helpers.rb +7 -3
  68. data/lib/active_support/time_with_zone.rb +19 -5
  69. data/lib/active_support/values/time_zone.rb +8 -1
  70. data/lib/active_support/xml_mini.rb +1 -4
  71. data/lib/active_support.rb +11 -0
  72. metadata +11 -6
  73. data/lib/active_support/core_ext/range/each.rb +0 -24
@@ -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
@@ -45,20 +62,14 @@ module ActiveSupport
45
62
  ActiveSupport.test_order ||= :random
46
63
  end
47
64
 
48
- if Minitest.respond_to? :run_order # MT6 API change
49
- def run_order # :nodoc:
50
- test_order
51
- end
52
- end
53
-
54
65
  # Parallelizes the test suite.
55
66
  #
56
67
  # Takes a +workers+ argument that controls how many times the process
57
68
  # is forked. For each process a new database will be created suffixed
58
69
  # with the worker number.
59
70
  #
60
- # test-database-0
61
- # test-database-1
71
+ # test-database_0
72
+ # test-database_1
62
73
  #
63
74
  # If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored
64
75
  # and the environment variable will be used instead. This is useful for CI
@@ -84,14 +95,45 @@ module ActiveSupport
84
95
  # Because parallelization presents an overhead, it is only enabled when the
85
96
  # number of tests to run is above the +threshold+ param. The default value is
86
97
  # 50, and it's configurable via +config.active_support.test_parallelization_threshold+.
87
- def parallelize(workers: :number_of_processors, with: :processes, threshold: ActiveSupport.test_parallelization_threshold)
88
- workers = Concurrent.processor_count if workers == :number_of_processors
89
- workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
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
90
118
 
91
119
  Minitest.parallel_executor = ActiveSupport::Testing::ParallelizeExecutor.new(size: workers, with: with, threshold: threshold)
92
120
  end
93
121
 
94
- # Set up hook for parallel testing. This can be used if you have multiple
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
95
137
  # databases or any behavior that needs to be run after the process is forked
96
138
  # but before the tests run.
97
139
  #
@@ -147,11 +189,18 @@ module ActiveSupport
147
189
 
148
190
  alias_method :method_name, :name
149
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
+
150
197
  include ActiveSupport::Testing::TaggedLogging
151
198
  prepend ActiveSupport::Testing::SetupAndTeardown
152
199
  prepend ActiveSupport::Testing::TestsWithoutAssertions
153
200
  include ActiveSupport::Testing::Assertions
154
201
  include ActiveSupport::Testing::ErrorReporterAssertions
202
+ include ActiveSupport::Testing::EventReporterAssertions
203
+ include ActiveSupport::Testing::NotificationAssertions
155
204
  include ActiveSupport::Testing::Deprecation
156
205
  include ActiveSupport::Testing::ConstantStubbing
157
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 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
@@ -120,8 +124,7 @@ module ActiveSupport
120
124
  actual = exp.call
121
125
  rich_message = -> do
122
126
  code_string = code.respond_to?(:call) ? _callable_to_source_string(code) : code
123
- error = "`#{code_string}` didn't change by #{diff}, but by #{actual - before_value}."
124
- error = "#{error}\n#{diff before_value + diff, actual}" if Minitest::VERSION > "6"
127
+ error = "`#{code_string}` didn't change by #{diff}, but by #{actual - before_value}"
125
128
  error = "#{message}.\n#{error}" if message
126
129
  error
127
130
  end
@@ -182,12 +185,24 @@ module ActiveSupport
182
185
  #
183
186
  # The keyword arguments +:from+ and +:to+ can be given to specify the
184
187
  # expected initial value and the expected value after the block was
185
- # executed.
188
+ # executed. The comparison is done using case equality (===), which means
189
+ # you can specify patterns or classes:
186
190
  #
191
+ # # Exact value match
187
192
  # assert_changes :@object, from: nil, to: :foo do
188
193
  # @object = :foo
189
194
  # end
190
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
+ #
191
206
  # An error message can be specified.
192
207
  #
193
208
  # assert_changes -> { Status.all_good? }, 'Expected the status to be bad' do
@@ -213,7 +228,7 @@ module ActiveSupport
213
228
  rich_message = -> do
214
229
  code_string = expression.respond_to?(:call) ? _callable_to_source_string(expression) : expression
215
230
  error = "`#{code_string}` didn't change"
216
- error = "#{error}. It was already #{to.inspect}." if before == to
231
+ error = "#{error}. It was already #{to.inspect}" if before == to
217
232
  error = "#{message}.\n#{error}" if message
218
233
  error
219
234
  end
@@ -239,12 +254,24 @@ module ActiveSupport
239
254
  # end
240
255
  #
241
256
  # Provide the optional keyword argument +:from+ to specify the expected
242
- # initial value.
257
+ # initial value. The comparison is done using case equality (===), which means
258
+ # you can specify patterns or classes:
243
259
  #
260
+ # # Exact value match
244
261
  # assert_no_changes -> { Status.all_good? }, from: true do
245
262
  # post :create, params: { status: { ok: true } }
246
263
  # end
247
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
+ #
248
275
  # An error message can be specified.
249
276
  #
250
277
  # assert_no_changes -> { Status.all_good? }, 'Expected the status to be good' do
@@ -269,9 +296,8 @@ module ActiveSupport
269
296
 
270
297
  rich_message = -> do
271
298
  code_string = expression.respond_to?(:call) ? _callable_to_source_string(expression) : expression
272
- error = "`#{code_string}` changed."
299
+ error = "`#{code_string}` changed"
273
300
  error = "#{message}.\n#{error}" if message
274
- error = "#{error}\n#{diff before, after}" if Minitest::VERSION > "6"
275
301
  error
276
302
  end
277
303
 
@@ -320,9 +346,6 @@ module ActiveSupport
320
346
  lines[0] = lines[0].byteslice(location[1]...)
321
347
  source = lines.join.strip
322
348
 
323
- # Strip stabby lambda from Ruby 4.1+
324
- source = source.sub(/^->\s*/, "")
325
-
326
349
  # We ignore procs defined with do/end as they are likely multi-line anyway.
327
350
  if source.start_with?("{")
328
351
  source.delete_suffix!("}")
@@ -2,9 +2,4 @@
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
10
5
  Minitest.autorun
@@ -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,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
@@ -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,7 +14,6 @@ module ActiveSupport
14
14
  def initialize
15
15
  @queue = Queue.new
16
16
  @active_workers = Concurrent::Map.new
17
- @worker_pids = Concurrent::Map.new
18
17
  @in_flight = Concurrent::Map.new
19
18
  end
20
19
 
@@ -41,24 +40,12 @@ module ActiveSupport
41
40
  end
42
41
  end
43
42
 
44
- def start_worker(worker_id, worker_pid)
43
+ def start_worker(worker_id)
45
44
  @active_workers[worker_id] = true
46
- @worker_pids[worker_id] = worker_pid
47
45
  end
48
46
 
49
- def stop_worker(worker_id, worker_pid)
47
+ def stop_worker(worker_id)
50
48
  @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
62
49
  end
63
50
 
64
51
  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, Process.pid)
21
+ @queue.start_worker(@id)
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, Process.pid)
32
+ @queue.stop_worker(@id)
33
33
  end
34
34
  end
35
35
 
@@ -47,11 +47,7 @@ module ActiveSupport
47
47
  set_process_title("#{klass}##{method}")
48
48
 
49
49
  result = klass.with_info_handler reporter do
50
- if Minitest.respond_to? :run_one_method
51
- Minitest.run_one_method klass, method
52
- else
53
- klass.new(method).run
54
- end
50
+ Minitest.run_one_method(klass, method)
55
51
  end
56
52
 
57
53
  safe_record(reporter, result)
@@ -82,6 +78,8 @@ module ActiveSupport
82
78
  end
83
79
 
84
80
  def after_fork
81
+ ActiveSupport::TestCase.parallel_worker_id = @number
82
+
85
83
  Parallelization.after_fork_hooks.each do |cb|
86
84
  cb.call(@number)
87
85
  end