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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +296 -172
  3. data/lib/active_support/backtrace_cleaner.rb +71 -0
  4. data/lib/active_support/cache/mem_cache_store.rb +13 -13
  5. data/lib/active_support/cache/redis_cache_store.rb +36 -30
  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/callbacks.rb +20 -8
  10. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  11. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  12. data/lib/active_support/configurable.rb +34 -0
  13. data/lib/active_support/continuous_integration.rb +145 -0
  14. data/lib/active_support/core_ext/array.rb +7 -7
  15. data/lib/active_support/core_ext/benchmark.rb +4 -12
  16. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  17. data/lib/active_support/core_ext/class.rb +2 -2
  18. data/lib/active_support/core_ext/date.rb +5 -5
  19. data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -35
  20. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  21. data/lib/active_support/core_ext/date_time.rb +5 -5
  22. data/lib/active_support/core_ext/digest.rb +1 -1
  23. data/lib/active_support/core_ext/erb/util.rb +3 -3
  24. data/lib/active_support/core_ext/file.rb +1 -1
  25. data/lib/active_support/core_ext/hash.rb +8 -8
  26. data/lib/active_support/core_ext/integer.rb +3 -3
  27. data/lib/active_support/core_ext/kernel.rb +3 -3
  28. data/lib/active_support/core_ext/module.rb +11 -11
  29. data/lib/active_support/core_ext/numeric.rb +3 -3
  30. data/lib/active_support/core_ext/object/json.rb +8 -1
  31. data/lib/active_support/core_ext/object/to_query.rb +5 -0
  32. data/lib/active_support/core_ext/object.rb +13 -13
  33. data/lib/active_support/core_ext/pathname.rb +2 -2
  34. data/lib/active_support/core_ext/range.rb +4 -5
  35. data/lib/active_support/core_ext/string/multibyte.rb +10 -1
  36. data/lib/active_support/core_ext/string/output_safety.rb +19 -12
  37. data/lib/active_support/core_ext/string.rb +13 -13
  38. data/lib/active_support/core_ext/symbol.rb +1 -1
  39. data/lib/active_support/core_ext/time/calculations.rb +0 -7
  40. data/lib/active_support/core_ext/time/compatibility.rb +2 -27
  41. data/lib/active_support/core_ext/time.rb +5 -5
  42. data/lib/active_support/core_ext.rb +1 -1
  43. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  44. data/lib/active_support/current_attributes.rb +13 -10
  45. data/lib/active_support/dependencies/interlock.rb +11 -5
  46. data/lib/active_support/dependencies.rb +6 -1
  47. data/lib/active_support/deprecation/reporting.rb +4 -2
  48. data/lib/active_support/deprecation.rb +1 -1
  49. data/lib/active_support/editor.rb +70 -0
  50. data/lib/active_support/error_reporter.rb +50 -6
  51. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  52. data/lib/active_support/event_reporter.rb +592 -0
  53. data/lib/active_support/evented_file_update_checker.rb +5 -1
  54. data/lib/active_support/execution_context.rb +64 -7
  55. data/lib/active_support/file_update_checker.rb +7 -5
  56. data/lib/active_support/gem_version.rb +2 -2
  57. data/lib/active_support/gzip.rb +1 -0
  58. data/lib/active_support/hash_with_indifferent_access.rb +27 -7
  59. data/lib/active_support/i18n_railtie.rb +1 -2
  60. data/lib/active_support/inflector/inflections.rb +31 -15
  61. data/lib/active_support/inflector/transliterate.rb +6 -8
  62. data/lib/active_support/isolated_execution_state.rb +12 -15
  63. data/lib/active_support/json/decoding.rb +2 -2
  64. data/lib/active_support/json/encoding.rb +135 -17
  65. data/lib/active_support/log_subscriber.rb +2 -6
  66. data/lib/active_support/message_encryptors.rb +52 -0
  67. data/lib/active_support/message_pack/extensions.rb +5 -0
  68. data/lib/active_support/message_verifiers.rb +52 -0
  69. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  70. data/lib/active_support/messages/rotator.rb +5 -0
  71. data/lib/active_support/multibyte/chars.rb +8 -1
  72. data/lib/active_support/multibyte.rb +4 -0
  73. data/lib/active_support/notifications/fanout.rb +64 -42
  74. data/lib/active_support/railtie.rb +32 -15
  75. data/lib/active_support/structured_event_subscriber.rb +99 -0
  76. data/lib/active_support/subscriber.rb +0 -5
  77. data/lib/active_support/syntax_error_proxy.rb +3 -0
  78. data/lib/active_support/test_case.rb +61 -6
  79. data/lib/active_support/testing/assertions.rb +34 -6
  80. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  81. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  82. data/lib/active_support/testing/notification_assertions.rb +92 -0
  83. data/lib/active_support/testing/parallelization/worker.rb +2 -0
  84. data/lib/active_support/testing/parallelization.rb +13 -0
  85. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  86. data/lib/active_support/testing/time_helpers.rb +7 -3
  87. data/lib/active_support/time_with_zone.rb +22 -22
  88. data/lib/active_support/values/time_zone.rb +8 -1
  89. data/lib/active_support/xml_mini.rb +1 -2
  90. data/lib/active_support.rb +20 -15
  91. metadata +24 -17
  92. 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 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
@@ -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
- raise Minitest::Assertion, "No error reporter is configured"
47
+ flunk("No error reporter is configured")
48
48
  end
49
49
  end
50
50
  end
@@ -102,6 +102,23 @@ module ActiveSupport
102
102
  assert(false, message)
103
103
  end
104
104
  end
105
+
106
+ # Captures reported errors from within the block that match the given
107
+ # error class.
108
+ #
109
+ # reports = capture_error_reports(IOError) do
110
+ # Rails.error.report(IOError.new("Oops"))
111
+ # Rails.error.report(IOError.new("Oh no"))
112
+ # Rails.error.report(StandardError.new)
113
+ # end
114
+ #
115
+ # assert_equal 2, reports.size
116
+ # assert_equal "Oops", reports.first.error.message
117
+ # assert_equal "Oh no", reports.last.error.message
118
+ def capture_error_reports(error_class = StandardError, &block)
119
+ reports = ErrorCollector.record(&block)
120
+ reports.select { |r| error_class === r.error }
121
+ end
105
122
  end
106
123
  end
107
124
  end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module Testing
5
+ # Provides test helpers for asserting on ActiveSupport::EventReporter events.
6
+ module EventReporterAssertions
7
+ module EventCollector # :nodoc:
8
+ @subscribed = false
9
+ @mutex = Mutex.new
10
+
11
+ class Event # :nodoc:
12
+ attr_reader :event_data
13
+
14
+ def initialize(event_data)
15
+ @event_data = event_data
16
+ end
17
+
18
+ def inspect
19
+ "#{event_data[:name]} (payload: #{event_data[:payload].inspect}, tags: #{event_data[:tags].inspect})"
20
+ end
21
+
22
+ def matches?(name, payload, tags)
23
+ return false unless name.to_s == event_data[:name]
24
+
25
+ if payload && payload.is_a?(Hash)
26
+ return false unless matches_hash?(payload, :payload)
27
+ end
28
+
29
+ return false unless matches_hash?(tags, :tags)
30
+ true
31
+ end
32
+
33
+ private
34
+ def matches_hash?(expected_hash, event_key)
35
+ expected_hash.all? do |k, v|
36
+ if v.is_a?(Regexp)
37
+ event_data.dig(event_key, k).to_s.match?(v)
38
+ else
39
+ event_data.dig(event_key, k) == v
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ class << self
46
+ def emit(event)
47
+ event_recorders&.each do |events|
48
+ events << Event.new(event)
49
+ end
50
+ true
51
+ end
52
+
53
+ def record
54
+ subscribe
55
+ events = []
56
+ event_recorders << events
57
+ begin
58
+ yield
59
+ events
60
+ ensure
61
+ event_recorders.delete_if { |r| events.equal?(r) }
62
+ end
63
+ end
64
+
65
+ private
66
+ def subscribe
67
+ return if @subscribed
68
+
69
+ @mutex.synchronize do
70
+ unless @subscribed
71
+ if ActiveSupport.event_reporter
72
+ ActiveSupport.event_reporter.subscribe(self)
73
+ @subscribed = true
74
+ else
75
+ raise Minitest::Assertion, "No event reporter is configured"
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ def event_recorders
82
+ ActiveSupport::IsolatedExecutionState[:active_support_event_reporter_assertions] ||= []
83
+ end
84
+ end
85
+ end
86
+
87
+ # Asserts that the block does not cause an event to be reported to +Rails.event+.
88
+ #
89
+ # If no name is provided, passes if evaluated code in the yielded block reports no events.
90
+ #
91
+ # assert_no_event_reported do
92
+ # service_that_does_not_report_events.perform
93
+ # end
94
+ #
95
+ # If a name is provided, passes if evaluated code in the yielded block reports no events
96
+ # with that name.
97
+ #
98
+ # assert_no_event_reported("user.created") do
99
+ # service_that_does_not_report_events.perform
100
+ # end
101
+ def assert_no_event_reported(name = nil, payload: {}, tags: {}, &block)
102
+ events = EventCollector.record(&block)
103
+
104
+ if name.nil?
105
+ assert_predicate(events, :empty?)
106
+ else
107
+ matching_event = events.find { |event| event.matches?(name, payload, tags) }
108
+ if matching_event
109
+ message = "Expected no '#{name}' event to be reported, but found:\n " \
110
+ "#{matching_event.inspect}"
111
+ flunk(message)
112
+ end
113
+ assert(true)
114
+ end
115
+ end
116
+
117
+ # Asserts that the block causes an event with the given name to be reported
118
+ # to +Rails.event+.
119
+ #
120
+ # Passes if the evaluated code in the yielded block reports a matching event.
121
+ #
122
+ # assert_event_reported("user.created") do
123
+ # Rails.event.notify("user.created", { id: 123 })
124
+ # end
125
+ #
126
+ # To test further details about the reported event, you can specify payload and tag matchers.
127
+ #
128
+ # assert_event_reported("user.created",
129
+ # payload: { id: 123, name: "John Doe" },
130
+ # tags: { request_id: /[0-9]+/ }
131
+ # ) do
132
+ # Rails.event.tagged(request_id: "123") do
133
+ # Rails.event.notify("user.created", { id: 123, name: "John Doe" })
134
+ # end
135
+ # end
136
+ #
137
+ # The matchers support partial matching - only the specified keys need to match.
138
+ #
139
+ # assert_event_reported("user.created", payload: { id: 123 }) do
140
+ # Rails.event.notify("user.created", { id: 123, name: "John Doe" })
141
+ # end
142
+ def assert_event_reported(name, payload: nil, tags: {}, &block)
143
+ events = EventCollector.record(&block)
144
+
145
+ if events.empty?
146
+ flunk("Expected an event to be reported, but there were no events reported.")
147
+ elsif (event = events.find { |event| event.matches?(name, payload, tags) })
148
+ assert(true)
149
+ event.event_data
150
+ else
151
+ message = "Expected an event to be reported matching:\n " \
152
+ "name: #{name.inspect}\n " \
153
+ "payload: #{payload.inspect}\n " \
154
+ "tags: #{tags.inspect}\n" \
155
+ "but none of the #{events.size} reported events matched:\n " \
156
+ "#{events.map(&:inspect).join("\n ")}"
157
+ flunk(message)
158
+ end
159
+ end
160
+
161
+ # Asserts that the provided events were reported, regardless of order.
162
+ #
163
+ # assert_events_reported([
164
+ # { name: "user.created", payload: { id: 123 } },
165
+ # { name: "email.sent", payload: { to: "user@example.com" } }
166
+ # ]) do
167
+ # create_user_and_send_welcome_email
168
+ # end
169
+ #
170
+ # Supports the same payload and tag matching as +assert_event_reported+.
171
+ #
172
+ # assert_events_reported([
173
+ # {
174
+ # name: "process.started",
175
+ # payload: { id: 123 },
176
+ # tags: { request_id: /[0-9]+/ }
177
+ # },
178
+ # { name: "process.completed" }
179
+ # ]) do
180
+ # Rails.event.tagged(request_id: "456") do
181
+ # start_and_complete_process(123)
182
+ # end
183
+ # end
184
+ def assert_events_reported(expected_events, &block)
185
+ events = EventCollector.record(&block)
186
+
187
+ if events.empty? && expected_events.size > 0
188
+ flunk("Expected #{expected_events.size} events to be reported, but there were no events reported.")
189
+ end
190
+
191
+ events_copy = events.dup
192
+
193
+ expected_events.each do |expected_event|
194
+ name = expected_event[:name]
195
+ payload = expected_event[:payload] || {}
196
+ tags = expected_event[:tags] || {}
197
+
198
+ matching_event_index = events_copy.find_index { |event| event.matches?(name, payload, tags) }
199
+
200
+ if matching_event_index
201
+ events_copy.delete_at(matching_event_index)
202
+ else
203
+ message = "Expected an event to be reported matching:\n " \
204
+ "name: #{name.inspect}\n " \
205
+ "payload: #{payload.inspect}\n " \
206
+ "tags: #{tags.inspect}\n" \
207
+ "but none of the #{events.size} reported events matched:\n " \
208
+ "#{events.map(&:inspect).join("\n ")}"
209
+ flunk(message)
210
+ end
211
+ end
212
+
213
+ assert(true)
214
+ end
215
+
216
+ # Allows debug events to be reported to +Rails.event+ for the duration of a given block.
217
+ #
218
+ # with_debug_event_reporting do
219
+ # service_that_reports_debug_events.perform
220
+ # end
221
+ #
222
+ def with_debug_event_reporting(&block)
223
+ ActiveSupport.event_reporter.with_debug(&block)
224
+ end
225
+ end
226
+ end
227
+ end
@@ -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
@@ -78,6 +78,8 @@ module ActiveSupport
78
78
  end
79
79
 
80
80
  def after_fork
81
+ ActiveSupport::TestCase.parallel_worker_id = @number
82
+
81
83
  Parallelization.after_fork_hooks.each do |cb|
82
84
  cb.call(@number)
83
85
  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 +Time.now+. Forwards optional <tt>with_usec</tt> argument.
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 Time.now, with_usec: with_usec, &block
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
- @period = @utc ? period : get_period_and_ensure_valid_local_time(period)
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
- zone == "UTC" || zone == "UCT"
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
- "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z')}"
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
- begin
303
- result = utc + other
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
- if preserve_timezone == :zone
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 time.kind_of?(Date)
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/Godthab",
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.parse(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 },