activesupport 8.0.4 → 8.1.0
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 +296 -172
- 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/callbacks.rb +20 -8
- data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
- data/lib/active_support/concurrency/thread_monitor.rb +55 -0
- data/lib/active_support/configurable.rb +34 -0
- data/lib/active_support/continuous_integration.rb +145 -0
- data/lib/active_support/core_ext/array.rb +7 -7
- data/lib/active_support/core_ext/benchmark.rb +4 -12
- data/lib/active_support/core_ext/big_decimal.rb +1 -1
- data/lib/active_support/core_ext/class.rb +2 -2
- 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.rb +5 -5
- data/lib/active_support/core_ext/digest.rb +1 -1
- data/lib/active_support/core_ext/erb/util.rb +3 -3
- data/lib/active_support/core_ext/file.rb +1 -1
- 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.rb +11 -11
- data/lib/active_support/core_ext/numeric.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/object.rb +13 -13
- data/lib/active_support/core_ext/pathname.rb +2 -2
- data/lib/active_support/core_ext/range.rb +4 -5
- 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/core_ext/string.rb +13 -13
- data/lib/active_support/core_ext/symbol.rb +1 -1
- data/lib/active_support/core_ext/time/calculations.rb +0 -7
- data/lib/active_support/core_ext/time/compatibility.rb +2 -27
- data/lib/active_support/core_ext/time.rb +5 -5
- data/lib/active_support/core_ext.rb +1 -1
- data/lib/active_support/current_attributes/test_helper.rb +2 -2
- data/lib/active_support/current_attributes.rb +13 -10
- data/lib/active_support/dependencies/interlock.rb +11 -5
- data/lib/active_support/dependencies.rb +6 -1
- 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 +592 -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 +7 -5
- data/lib/active_support/gem_version.rb +2 -2
- 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 +12 -15
- data/lib/active_support/json/decoding.rb +2 -2
- data/lib/active_support/json/encoding.rb +135 -17
- data/lib/active_support/log_subscriber.rb +2 -6
- 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/notifications/fanout.rb +64 -42
- data/lib/active_support/railtie.rb +32 -15
- 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 +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 +227 -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 +22 -22
- data/lib/active_support/values/time_zone.rb +8 -1
- data/lib/active_support/xml_mini.rb +1 -2
- data/lib/active_support.rb +20 -15
- metadata +24 -17
- data/lib/active_support/core_ext/range/each.rb +0 -24
|
@@ -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,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
|
|
@@ -9,6 +9,14 @@ require "active_support/testing/parallelization/worker"
|
|
|
9
9
|
module ActiveSupport
|
|
10
10
|
module Testing
|
|
11
11
|
class Parallelization # :nodoc:
|
|
12
|
+
@@before_fork_hooks = []
|
|
13
|
+
|
|
14
|
+
def self.before_fork_hook(&blk)
|
|
15
|
+
@@before_fork_hooks << blk
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
cattr_reader :before_fork_hooks
|
|
19
|
+
|
|
12
20
|
@@after_fork_hooks = []
|
|
13
21
|
|
|
14
22
|
def self.after_fork_hook(&blk)
|
|
@@ -32,7 +40,12 @@ module ActiveSupport
|
|
|
32
40
|
@url = DRb.start_service("drbunix:", @queue_server).uri
|
|
33
41
|
end
|
|
34
42
|
|
|
43
|
+
def before_fork
|
|
44
|
+
Parallelization.before_fork_hooks.each(&:call)
|
|
45
|
+
end
|
|
46
|
+
|
|
35
47
|
def start
|
|
48
|
+
before_fork
|
|
36
49
|
@worker_pool = @worker_count.times.map do |worker|
|
|
37
50
|
Worker.new(worker, @url).start
|
|
38
51
|
end
|
|
@@ -11,7 +11,7 @@ module ActiveSupport
|
|
|
11
11
|
|
|
12
12
|
if assertions.zero? && !skipped? && !error?
|
|
13
13
|
file, line = method(name).source_location
|
|
14
|
-
warn "Test is missing assertions: `#{name}` #{file}:#{line}"
|
|
14
|
+
warn "Test is missing assertions: `#{name}` #{File.expand_path(file)}:#{line}"
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -238,12 +238,16 @@ module ActiveSupport
|
|
|
238
238
|
end
|
|
239
239
|
alias_method :unfreeze_time, :travel_back
|
|
240
240
|
|
|
241
|
-
# Calls +travel_to+ with +
|
|
241
|
+
# Calls +travel_to+ with +date_or_time+, which defaults to +Time.now+.
|
|
242
|
+
# Forwards optional <tt>with_usec</tt> argument.
|
|
242
243
|
#
|
|
243
244
|
# Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
|
|
244
245
|
# freeze_time
|
|
245
246
|
# sleep(1)
|
|
246
247
|
# Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
|
|
248
|
+
# freeze_time Time.current + 1.day
|
|
249
|
+
# sleep(1)
|
|
250
|
+
# Time.current # => Mon, 10 Jul 2017 15:34:49 EST -05:00
|
|
247
251
|
#
|
|
248
252
|
# This method also accepts a block, which will return the current time back to its original
|
|
249
253
|
# state at the end of the block:
|
|
@@ -254,8 +258,8 @@ module ActiveSupport
|
|
|
254
258
|
# User.create.created_at # => Sun, 09 Jul 2017 15:34:49 EST -05:00
|
|
255
259
|
# end
|
|
256
260
|
# Time.current # => Sun, 09 Jul 2017 15:34:50 EST -05:00
|
|
257
|
-
def freeze_time(with_usec: false, &block)
|
|
258
|
-
travel_to
|
|
261
|
+
def freeze_time(date_or_time = Time.now, with_usec: false, &block)
|
|
262
|
+
travel_to date_or_time, with_usec: with_usec, &block
|
|
259
263
|
end
|
|
260
264
|
|
|
261
265
|
private
|
|
@@ -49,9 +49,15 @@ module ActiveSupport
|
|
|
49
49
|
attr_reader :time_zone
|
|
50
50
|
|
|
51
51
|
def initialize(utc_time, time_zone, local_time = nil, period = nil)
|
|
52
|
-
@utc = utc_time ? transfer_time_values_to_utc_constructor(utc_time) : nil
|
|
53
52
|
@time_zone, @time = time_zone, local_time
|
|
54
|
-
|
|
53
|
+
if utc_time
|
|
54
|
+
@utc = transfer_time_values_to_utc_constructor(utc_time)
|
|
55
|
+
@period = period
|
|
56
|
+
else
|
|
57
|
+
@utc = nil
|
|
58
|
+
@period = get_period_and_ensure_valid_local_time(period)
|
|
59
|
+
end
|
|
60
|
+
@is_utc = zone == "UTC" || zone == "UCT"
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
# Returns a <tt>Time</tt> instance that represents the time in +time_zone+.
|
|
@@ -103,7 +109,7 @@ module ActiveSupport
|
|
|
103
109
|
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
|
|
104
110
|
# Time.zone.now.utc? # => false
|
|
105
111
|
def utc?
|
|
106
|
-
|
|
112
|
+
@is_utc
|
|
107
113
|
end
|
|
108
114
|
alias_method :gmt?, :utc?
|
|
109
115
|
|
|
@@ -146,7 +152,13 @@ module ActiveSupport
|
|
|
146
152
|
#
|
|
147
153
|
# Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00"
|
|
148
154
|
def xmlschema(fraction_digits = 0)
|
|
149
|
-
|
|
155
|
+
if @is_utc
|
|
156
|
+
utc.iso8601(fraction_digits || 0)
|
|
157
|
+
else
|
|
158
|
+
str = time.iso8601(fraction_digits || 0)
|
|
159
|
+
str[-1] = formatted_offset(true, "Z")
|
|
160
|
+
str
|
|
161
|
+
end
|
|
150
162
|
end
|
|
151
163
|
alias_method :iso8601, :xmlschema
|
|
152
164
|
alias_method :rfc3339, :xmlschema
|
|
@@ -299,16 +311,8 @@ module ActiveSupport
|
|
|
299
311
|
if duration_of_variable_length?(other)
|
|
300
312
|
method_missing(:+, other)
|
|
301
313
|
else
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
rescue TypeError
|
|
305
|
-
result = utc.to_datetime.since(other)
|
|
306
|
-
ActiveSupport.deprecator.warn(
|
|
307
|
-
"Adding an instance of #{other.class} to an instance of #{self.class} is deprecated. This behavior will raise " \
|
|
308
|
-
"a `TypeError` in Rails 8.1."
|
|
309
|
-
)
|
|
310
|
-
result.in_time_zone(time_zone)
|
|
311
|
-
end
|
|
314
|
+
result = utc + other
|
|
315
|
+
|
|
312
316
|
result.in_time_zone(time_zone)
|
|
313
317
|
end
|
|
314
318
|
end
|
|
@@ -491,13 +495,7 @@ module ActiveSupport
|
|
|
491
495
|
# with the same UTC offset as +self+ or in the local system timezone
|
|
492
496
|
# depending on the setting of +ActiveSupport.to_time_preserves_timezone+.
|
|
493
497
|
def to_time
|
|
494
|
-
|
|
495
|
-
@to_time_with_timezone ||= getlocal(time_zone)
|
|
496
|
-
elsif preserve_timezone
|
|
497
|
-
@to_time_with_instance_offset ||= getlocal(utc_offset)
|
|
498
|
-
else
|
|
499
|
-
@to_time_with_system_offset ||= getlocal
|
|
500
|
-
end
|
|
498
|
+
@to_time_with_timezone ||= getlocal(time_zone)
|
|
501
499
|
end
|
|
502
500
|
|
|
503
501
|
# So that +self+ <tt>acts_like?(:time)</tt>.
|
|
@@ -560,7 +558,9 @@ module ActiveSupport
|
|
|
560
558
|
SECONDS_PER_DAY = 86400
|
|
561
559
|
|
|
562
560
|
def incorporate_utc_offset(time, offset)
|
|
563
|
-
if
|
|
561
|
+
if offset.zero?
|
|
562
|
+
time
|
|
563
|
+
elsif time.kind_of?(Date)
|
|
564
564
|
time + Rational(offset, SECONDS_PER_DAY)
|
|
565
565
|
else
|
|
566
566
|
time + offset
|
|
@@ -57,13 +57,14 @@ module ActiveSupport
|
|
|
57
57
|
"Caracas" => "America/Caracas",
|
|
58
58
|
"La Paz" => "America/La_Paz",
|
|
59
59
|
"Santiago" => "America/Santiago",
|
|
60
|
+
"Asuncion" => "America/Asuncion",
|
|
60
61
|
"Newfoundland" => "America/St_Johns",
|
|
61
62
|
"Brasilia" => "America/Sao_Paulo",
|
|
62
63
|
"Buenos Aires" => "America/Argentina/Buenos_Aires",
|
|
63
64
|
"Montevideo" => "America/Montevideo",
|
|
64
65
|
"Georgetown" => "America/Guyana",
|
|
65
66
|
"Puerto Rico" => "America/Puerto_Rico",
|
|
66
|
-
"Greenland" => "America/
|
|
67
|
+
"Greenland" => "America/Nuuk",
|
|
67
68
|
"Mid-Atlantic" => "Atlantic/South_Georgia",
|
|
68
69
|
"Azores" => "Atlantic/Azores",
|
|
69
70
|
"Cape Verde Is." => "Atlantic/Cape_Verde",
|
|
@@ -313,6 +314,12 @@ module ActiveSupport
|
|
|
313
314
|
end
|
|
314
315
|
# :startdoc:
|
|
315
316
|
|
|
317
|
+
# Returns a standard time zone name defined by IANA
|
|
318
|
+
# https://www.iana.org/time-zones
|
|
319
|
+
def standard_name
|
|
320
|
+
MAPPING[name] || name
|
|
321
|
+
end
|
|
322
|
+
|
|
316
323
|
# Returns the offset of this time zone from UTC in seconds.
|
|
317
324
|
def utc_offset
|
|
318
325
|
@utc_offset || tzinfo&.current_period&.base_utc_offset
|
|
@@ -62,11 +62,10 @@ module ActiveSupport
|
|
|
62
62
|
"yaml" => Proc.new { |yaml| yaml.to_yaml }
|
|
63
63
|
} unless defined?(FORMATTING)
|
|
64
64
|
|
|
65
|
-
# TODO use regexp instead of Date.parse
|
|
66
65
|
unless defined?(PARSING)
|
|
67
66
|
PARSING = {
|
|
68
67
|
"symbol" => Proc.new { |symbol| symbol.to_s.to_sym },
|
|
69
|
-
"date" => Proc.new { |date| ::Date.
|
|
68
|
+
"date" => Proc.new { |date| ::Date.strptime(date, "%Y-%m-%d") },
|
|
70
69
|
"datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc },
|
|
71
70
|
"duration" => Proc.new { |duration| Duration.parse(duration) },
|
|
72
71
|
"integer" => Proc.new { |integer| integer.to_i },
|