activesupport 8.0.2 → 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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +333 -128
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +71 -0
  5. data/lib/active_support/broadcast_logger.rb +46 -59
  6. data/lib/active_support/cache/mem_cache_store.rb +25 -27
  7. data/lib/active_support/cache/redis_cache_store.rb +36 -30
  8. data/lib/active_support/cache/strategy/local_cache.rb +16 -7
  9. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  10. data/lib/active_support/cache.rb +70 -6
  11. data/lib/active_support/callbacks.rb +20 -8
  12. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  13. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  14. data/lib/active_support/configurable.rb +34 -0
  15. data/lib/active_support/continuous_integration.rb +145 -0
  16. data/lib/active_support/core_ext/array.rb +7 -7
  17. data/lib/active_support/core_ext/benchmark.rb +4 -11
  18. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  19. data/lib/active_support/core_ext/class/attribute.rb +8 -6
  20. data/lib/active_support/core_ext/class.rb +2 -2
  21. data/lib/active_support/core_ext/date.rb +5 -5
  22. data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -35
  23. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  24. data/lib/active_support/core_ext/date_time/conversions.rb +4 -2
  25. data/lib/active_support/core_ext/date_time.rb +5 -5
  26. data/lib/active_support/core_ext/digest.rb +1 -1
  27. data/lib/active_support/core_ext/enumerable.rb +16 -4
  28. data/lib/active_support/core_ext/erb/util.rb +3 -3
  29. data/lib/active_support/core_ext/file.rb +1 -1
  30. data/lib/active_support/core_ext/hash.rb +8 -8
  31. data/lib/active_support/core_ext/integer.rb +3 -3
  32. data/lib/active_support/core_ext/kernel.rb +3 -3
  33. data/lib/active_support/core_ext/module.rb +11 -11
  34. data/lib/active_support/core_ext/numeric.rb +3 -3
  35. data/lib/active_support/core_ext/object/json.rb +8 -1
  36. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  37. data/lib/active_support/core_ext/object/try.rb +2 -2
  38. data/lib/active_support/core_ext/object.rb +13 -13
  39. data/lib/active_support/core_ext/pathname.rb +2 -2
  40. data/lib/active_support/core_ext/range/overlap.rb +3 -3
  41. data/lib/active_support/core_ext/range/sole.rb +17 -0
  42. data/lib/active_support/core_ext/range.rb +4 -4
  43. data/lib/active_support/core_ext/string/filters.rb +3 -3
  44. data/lib/active_support/core_ext/string/multibyte.rb +12 -3
  45. data/lib/active_support/core_ext/string/output_safety.rb +19 -12
  46. data/lib/active_support/core_ext/string.rb +13 -13
  47. data/lib/active_support/core_ext/symbol.rb +1 -1
  48. data/lib/active_support/core_ext/time/calculations.rb +0 -7
  49. data/lib/active_support/core_ext/time/compatibility.rb +2 -27
  50. data/lib/active_support/core_ext/time.rb +5 -5
  51. data/lib/active_support/core_ext.rb +1 -1
  52. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  53. data/lib/active_support/current_attributes.rb +26 -16
  54. data/lib/active_support/dependencies/interlock.rb +11 -5
  55. data/lib/active_support/dependencies.rb +6 -1
  56. data/lib/active_support/deprecation/reporting.rb +4 -2
  57. data/lib/active_support/deprecation.rb +1 -1
  58. data/lib/active_support/editor.rb +70 -0
  59. data/lib/active_support/error_reporter.rb +50 -6
  60. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  61. data/lib/active_support/event_reporter.rb +592 -0
  62. data/lib/active_support/evented_file_update_checker.rb +5 -1
  63. data/lib/active_support/execution_context.rb +64 -7
  64. data/lib/active_support/file_update_checker.rb +8 -6
  65. data/lib/active_support/gem_version.rb +2 -2
  66. data/lib/active_support/gzip.rb +1 -0
  67. data/lib/active_support/hash_with_indifferent_access.rb +47 -24
  68. data/lib/active_support/i18n_railtie.rb +1 -2
  69. data/lib/active_support/inflector/inflections.rb +31 -15
  70. data/lib/active_support/inflector/transliterate.rb +6 -8
  71. data/lib/active_support/isolated_execution_state.rb +12 -15
  72. data/lib/active_support/json/decoding.rb +6 -4
  73. data/lib/active_support/json/encoding.rb +135 -17
  74. data/lib/active_support/lazy_load_hooks.rb +1 -1
  75. data/lib/active_support/log_subscriber.rb +2 -6
  76. data/lib/active_support/logger_thread_safe_level.rb +6 -3
  77. data/lib/active_support/message_encryptors.rb +52 -0
  78. data/lib/active_support/message_pack/extensions.rb +5 -0
  79. data/lib/active_support/message_verifiers.rb +52 -0
  80. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  81. data/lib/active_support/messages/rotator.rb +5 -0
  82. data/lib/active_support/multibyte/chars.rb +8 -1
  83. data/lib/active_support/multibyte.rb +4 -0
  84. data/lib/active_support/notifications/fanout.rb +64 -42
  85. data/lib/active_support/notifications/instrumenter.rb +1 -1
  86. data/lib/active_support/railtie.rb +32 -15
  87. data/lib/active_support/structured_event_subscriber.rb +99 -0
  88. data/lib/active_support/subscriber.rb +0 -5
  89. data/lib/active_support/syntax_error_proxy.rb +3 -0
  90. data/lib/active_support/test_case.rb +61 -6
  91. data/lib/active_support/testing/assertions.rb +34 -6
  92. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  93. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  94. data/lib/active_support/testing/notification_assertions.rb +92 -0
  95. data/lib/active_support/testing/parallelization/server.rb +15 -2
  96. data/lib/active_support/testing/parallelization/worker.rb +4 -2
  97. data/lib/active_support/testing/parallelization.rb +25 -1
  98. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  99. data/lib/active_support/testing/time_helpers.rb +7 -3
  100. data/lib/active_support/time_with_zone.rb +22 -22
  101. data/lib/active_support/values/time_zone.rb +8 -1
  102. data/lib/active_support/xml_mini.rb +3 -2
  103. data/lib/active_support.rb +20 -15
  104. metadata +27 -19
  105. data/lib/active_support/core_ext/range/each.rb +0 -24
@@ -35,6 +35,8 @@ module ActiveSupport
35
35
  :race_condition_ttl,
36
36
  :serializer,
37
37
  :skip_nil,
38
+ :raw,
39
+ :max_key_size,
38
40
  ]
39
41
 
40
42
  # Mapping of canonical option names to aliases that a store will recognize.
@@ -186,6 +188,12 @@ module ActiveSupport
186
188
  # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
187
189
  #
188
190
  class Store
191
+ # Default +ConnectionPool+ options
192
+ DEFAULT_POOL_OPTIONS = { size: 5, timeout: 5 }.freeze
193
+
194
+ # Keys are truncated with the Active Support digest if they exceed the limit.
195
+ MAX_KEY_SIZE = 250
196
+
189
197
  cattr_accessor :logger, instance_writer: true
190
198
  cattr_accessor :raise_on_invalid_cache_expiration_time, default: false
191
199
 
@@ -194,9 +202,6 @@ module ActiveSupport
194
202
 
195
203
  class << self
196
204
  private
197
- DEFAULT_POOL_OPTIONS = { size: 5, timeout: 5 }.freeze
198
- private_constant :DEFAULT_POOL_OPTIONS
199
-
200
205
  def retrieve_pool_options(options)
201
206
  if options.key?(:pool)
202
207
  pool_options = options.delete(:pool)
@@ -298,6 +303,9 @@ module ActiveSupport
298
303
  @options[:compress] = true unless @options.key?(:compress)
299
304
  @options[:compress_threshold] ||= DEFAULT_COMPRESS_LIMIT
300
305
 
306
+ @max_key_size = @options.delete(:max_key_size)
307
+ @max_key_size = MAX_KEY_SIZE if @max_key_size.nil? # allow 'false' as a value
308
+
301
309
  @coder = @options.delete(:coder) do
302
310
  legacy_serializer = Cache.format_version < 7.1 && !@options[:serializer]
303
311
  serializer = @options.delete(:serializer) || default_serializer
@@ -556,7 +564,7 @@ module ActiveSupport
556
564
 
557
565
  instrument_multi :write_multi, normalized_hash, options do |payload|
558
566
  entries = hash.each_with_object({}) do |(name, value), memo|
559
- memo[normalize_key(name, options)] = Entry.new(value, **options.merge(version: normalize_version(name, options)))
567
+ memo[normalize_key(name, options)] = Entry.new(value, **options, version: normalize_version(name, options))
560
568
  end
561
569
 
562
570
  write_multi_entries entries, **options
@@ -658,13 +666,15 @@ module ActiveSupport
658
666
  # version, the read will be treated as a cache miss. This feature is
659
667
  # used to support recyclable cache keys.
660
668
  #
669
+ # * +:unless_exist+ - Prevents overwriting an existing cache entry.
670
+ #
661
671
  # Other options will be handled by the specific cache store implementation.
662
672
  def write(name, value, options = nil)
663
673
  options = merged_options(options)
664
674
  key = normalize_key(name, options)
665
675
 
666
676
  instrument(:write, key, options) do
667
- entry = Entry.new(value, **options.merge(version: normalize_version(name, options)))
677
+ entry = Entry.new(value, **options, version: normalize_version(name, options))
668
678
  write_entry(key, entry, **options)
669
679
  end
670
680
  end
@@ -741,6 +751,32 @@ module ActiveSupport
741
751
  raise NotImplementedError.new("#{self.class.name} does not support decrement")
742
752
  end
743
753
 
754
+ # Reads a counter that was set by #increment / #decrement.
755
+ #
756
+ # cache.write_counter("foo", 1)
757
+ # cache.read_counter("foo") # => 1
758
+ # cache.increment("foo")
759
+ # cache.read_counter("foo") # => 2
760
+ #
761
+ # Options are passed to the underlying cache implementation.
762
+ def read_counter(name, **options)
763
+ options = merged_options(options).merge(raw: true)
764
+ read(name, **options)&.to_i
765
+ end
766
+
767
+ # Writes a counter that can then be modified by #increment / #decrement.
768
+ #
769
+ # cache.write_counter("foo", 1)
770
+ # cache.read_counter("foo") # => 1
771
+ # cache.increment("foo")
772
+ # cache.read_counter("foo") # => 2
773
+ #
774
+ # Options are passed to the underlying cache implementation.
775
+ def write_counter(name, value, **options)
776
+ options = merged_options(options).merge(raw: true)
777
+ write(name, value.to_i, **options)
778
+ end
779
+
744
780
  # Cleans up the cache by removing expired entries.
745
781
  #
746
782
  # Options are passed to the underlying cache implementation.
@@ -760,6 +796,17 @@ module ActiveSupport
760
796
  raise NotImplementedError.new("#{self.class.name} does not support clear")
761
797
  end
762
798
 
799
+ # Get the current namespace
800
+ def namespace
801
+ @options[:namespace]
802
+ end
803
+
804
+ # Set the current namespace. Note, this will be ignored if custom
805
+ # options are passed to cache wills with a namespace key.
806
+ def namespace=(namespace)
807
+ @options[:namespace] = namespace
808
+ end
809
+
763
810
  private
764
811
  def default_serializer
765
812
  case Cache.format_version
@@ -926,16 +973,33 @@ module ActiveSupport
926
973
  options
927
974
  end
928
975
 
929
- # Expands and namespaces the cache key.
976
+ # Expands, namespaces and truncates the cache key.
930
977
  # Raises an exception when the key is +nil+ or an empty string.
931
978
  # May be overridden by cache stores to do additional normalization.
932
979
  def normalize_key(key, options = nil)
980
+ key = expand_and_namespace_key(key, options)
981
+ truncate_key(key)
982
+ end
983
+
984
+ def expand_and_namespace_key(key, options = nil)
933
985
  str_key = expanded_key(key)
934
986
  raise(ArgumentError, "key cannot be blank") if !str_key || str_key.empty?
935
987
 
936
988
  namespace_key str_key, options
937
989
  end
938
990
 
991
+ def truncate_key(key)
992
+ if key && @max_key_size && key.bytesize > @max_key_size
993
+ suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
994
+ truncate_at = @max_key_size - suffix.bytesize
995
+ key = key.byteslice(0, truncate_at)
996
+ key.scrub!("")
997
+ "#{key}#{suffix}"
998
+ else
999
+ key
1000
+ end
1001
+ end
1002
+
939
1003
  # Prefix the key with a namespace string:
940
1004
  #
941
1005
  # namespace_key 'foo', namespace: 'cache'
@@ -4,6 +4,7 @@ require "active_support/concern"
4
4
  require "active_support/descendants_tracker"
5
5
  require "active_support/core_ext/array/extract_options"
6
6
  require "active_support/core_ext/class/attribute"
7
+ require "active_support/core_ext/module/redefine_method"
7
8
  require "active_support/core_ext/string/filters"
8
9
  require "active_support/core_ext/object/blank"
9
10
 
@@ -573,7 +574,7 @@ module ActiveSupport
573
574
  @name = name
574
575
  @config = {
575
576
  scope: [:kind],
576
- terminator: default_terminator
577
+ terminator: DEFAULT_TERMINATOR
577
578
  }.merge!(config)
578
579
  @chain = []
579
580
  @all_callbacks = nil
@@ -661,8 +662,8 @@ module ActiveSupport
661
662
  @chain.delete_if { |c| callback.duplicates?(c) }
662
663
  end
663
664
 
664
- def default_terminator
665
- Proc.new do |target, result_lambda|
665
+ class DefaultTerminator # :nodoc:
666
+ def call(target, result_lambda)
666
667
  terminate = true
667
668
  catch(:abort) do
668
669
  result_lambda.call
@@ -671,6 +672,7 @@ module ActiveSupport
671
672
  terminate
672
673
  end
673
674
  end
675
+ DEFAULT_TERMINATOR = DefaultTerminator.new.freeze
674
676
  end
675
677
 
676
678
  module ClassMethods
@@ -904,12 +906,13 @@ module ActiveSupport
904
906
  names.each do |name|
905
907
  name = name.to_sym
906
908
 
907
- ([self] + self.descendants).each do |target|
908
- target.set_callbacks name, CallbackChain.new(name, options)
909
- end
910
-
911
909
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
912
- def _run_#{name}_callbacks(&block)
910
+ def _run_#{name}_callbacks
911
+ yield if block_given?
912
+ end
913
+ silence_redefinition_of_method(:_run_#{name}_callbacks)
914
+
915
+ def _run_#{name}_callbacks!(&block)
913
916
  run_callbacks #{name.inspect}, &block
914
917
  end
915
918
 
@@ -925,6 +928,10 @@ module ActiveSupport
925
928
  __callbacks[#{name.inspect}]
926
929
  end
927
930
  RUBY
931
+
932
+ ([self] + self.descendants).each do |target|
933
+ target.set_callbacks name, CallbackChain.new(name, options)
934
+ end
928
935
  end
929
936
  end
930
937
 
@@ -940,6 +947,11 @@ module ActiveSupport
940
947
  unless singleton_class.private_method_defined?(:__class_attr__callbacks, false)
941
948
  self.__callbacks = __callbacks.dup
942
949
  end
950
+ name = name.to_sym
951
+ callbacks_was = self.__callbacks[name.to_sym]
952
+ if (callbacks_was.nil? || callbacks_was.empty?) && !callbacks.empty?
953
+ alias_method("_run_#{name}_callbacks", "_run_#{name}_callbacks!")
954
+ end
943
955
  self.__callbacks[name.to_sym] = callbacks
944
956
  self.__callbacks
945
957
  end
@@ -4,69 +4,15 @@ require "monitor"
4
4
 
5
5
  module ActiveSupport
6
6
  module Concurrency
7
- module LoadInterlockAwareMonitorMixin # :nodoc:
8
- EXCEPTION_NEVER = { Exception => :never }.freeze
9
- EXCEPTION_IMMEDIATE = { Exception => :immediate }.freeze
10
- private_constant :EXCEPTION_NEVER, :EXCEPTION_IMMEDIATE
11
-
12
- # Enters an exclusive section, but allows dependency loading while blocked
13
- def mon_enter
14
- mon_try_enter ||
15
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads { super }
16
- end
17
-
18
- def synchronize(&block)
19
- Thread.handle_interrupt(EXCEPTION_NEVER) do
20
- mon_enter
21
-
22
- begin
23
- Thread.handle_interrupt(EXCEPTION_IMMEDIATE, &block)
24
- ensure
25
- mon_exit
26
- end
27
- end
28
- end
29
- end
30
7
  # A monitor that will permit dependency loading while blocked waiting for
31
8
  # the lock.
32
- class LoadInterlockAwareMonitor < Monitor
33
- include LoadInterlockAwareMonitorMixin
34
- end
35
-
36
- class ThreadLoadInterlockAwareMonitor # :nodoc:
37
- prepend LoadInterlockAwareMonitorMixin
38
-
39
- def initialize
40
- @owner = nil
41
- @count = 0
42
- @mutex = Mutex.new
43
- end
44
-
45
- private
46
- def mon_try_enter
47
- if @owner != Thread.current
48
- return false unless @mutex.try_lock
49
- @owner = Thread.current
50
- end
51
- @count += 1
52
- end
53
-
54
- def mon_enter
55
- @mutex.lock if @owner != Thread.current
56
- @owner = Thread.current
57
- @count += 1
58
- end
59
-
60
- def mon_exit
61
- unless @owner == Thread.current
62
- raise ThreadError, "current thread not owner"
63
- end
64
-
65
- @count -= 1
66
- return unless @count == 0
67
- @owner = nil
68
- @mutex.unlock
69
- end
70
- end
9
+ LoadInterlockAwareMonitor = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
10
+ "ActiveSupport::Concurrency::LoadInterlockAwareMonitor",
11
+ "::Monitor",
12
+ ActiveSupport.deprecator,
13
+ message: "ActiveSupport::Concurrency::LoadInterlockAwareMonitor is deprecated and will be " \
14
+ "removed in Rails 9.0. Use Monitor directly instead, as the loading interlock is " \
15
+ "no longer used."
16
+ )
71
17
  end
72
18
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module Concurrency
5
+ class ThreadMonitor # :nodoc:
6
+ EXCEPTION_NEVER = { Exception => :never }.freeze
7
+ EXCEPTION_IMMEDIATE = { Exception => :immediate }.freeze
8
+ private_constant :EXCEPTION_NEVER, :EXCEPTION_IMMEDIATE
9
+
10
+ def initialize
11
+ @owner = nil
12
+ @count = 0
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def synchronize(&block)
17
+ Thread.handle_interrupt(EXCEPTION_NEVER) do
18
+ mon_enter
19
+
20
+ begin
21
+ Thread.handle_interrupt(EXCEPTION_IMMEDIATE, &block)
22
+ ensure
23
+ mon_exit
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+ def mon_try_enter
30
+ if @owner != Thread.current
31
+ return false unless @mutex.try_lock
32
+ @owner = Thread.current
33
+ end
34
+ @count += 1
35
+ end
36
+
37
+ def mon_enter
38
+ @mutex.lock if @owner != Thread.current
39
+ @owner = Thread.current
40
+ @count += 1
41
+ end
42
+
43
+ def mon_exit
44
+ unless @owner == Thread.current
45
+ raise ThreadError, "current thread not owner"
46
+ end
47
+
48
+ @count -= 1
49
+ return unless @count == 0
50
+ @owner = nil
51
+ @mutex.unlock
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ActiveSupport.deprecator.warn <<~MSG
4
+ ActiveSupport::Configurable is deprecated without replacement, and will be removed in Rails 8.2.
5
+
6
+ You can emulate the previous behavior with `class_attribute`.
7
+ MSG
8
+
3
9
  require "active_support/concern"
4
10
  require "active_support/ordered_options"
5
11
 
@@ -27,6 +33,19 @@ module ActiveSupport
27
33
  end
28
34
 
29
35
  module ClassMethods
36
+ # Reads and writes attributes from a configuration OrderedOptions.
37
+ #
38
+ # require "active_support/configurable"
39
+ #
40
+ # class User
41
+ # include ActiveSupport::Configurable
42
+ # end
43
+ #
44
+ # User.config.allowed_access = true
45
+ # User.config.level = 1
46
+ #
47
+ # User.config.allowed_access # => true
48
+ # User.config.level # => 1
30
49
  def config
31
50
  @_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
32
51
  superclass.config.inheritable_copy
@@ -36,6 +55,21 @@ module ActiveSupport
36
55
  end
37
56
  end
38
57
 
58
+ # Configure values from within the passed block.
59
+ #
60
+ # require "active_support/configurable"
61
+ #
62
+ # class User
63
+ # include ActiveSupport::Configurable
64
+ # end
65
+ #
66
+ # User.allowed_access # => nil
67
+ #
68
+ # User.configure do |config|
69
+ # config.allowed_access = true
70
+ # end
71
+ #
72
+ # User.allowed_access # => true
39
73
  def configure
40
74
  yield config
41
75
  end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ # Provides a DSL for declaring a continuous integration workflow that can be run either locally or in the cloud.
5
+ # Each step is timed, reports success/error, and is aggregated into a collective report that reports total runtime,
6
+ # as well as whether the entire run was successful or not.
7
+ #
8
+ # Example:
9
+ #
10
+ # ActiveSupport::ContinuousIntegration.run do
11
+ # step "Setup", "bin/setup --skip-server"
12
+ # step "Style: Ruby", "bin/rubocop"
13
+ # step "Security: Gem audit", "bin/bundler-audit"
14
+ # step "Tests: Rails", "bin/rails test test:system"
15
+ #
16
+ # if success?
17
+ # step "Signoff: Ready for merge and deploy", "gh signoff"
18
+ # else
19
+ # failure "Skipping signoff; CI failed.", "Fix the issues and try again."
20
+ # end
21
+ # end
22
+ #
23
+ # Starting with Rails 8.1, a default `bin/ci` and `config/ci.rb` file are created to provide out-of-the-box CI.
24
+ class ContinuousIntegration
25
+ COLORS = {
26
+ banner: "\033[1;32m", # Green
27
+ title: "\033[1;35m", # Purple
28
+ subtitle: "\033[1;90m", # Medium Gray
29
+ error: "\033[1;31m", # Red
30
+ success: "\033[1;32m" # Green
31
+ }
32
+
33
+ attr_reader :results
34
+
35
+ # Perform a CI run. Execute each step, show their results and runtime, and exit with a non-zero status if there are any failures.
36
+ #
37
+ # Pass an optional title, subtitle, and a block that declares the steps to be executed.
38
+ #
39
+ # Sets the CI environment variable to "true" to allow for conditional behavior in the app, like enabling eager loading and disabling logging.
40
+ #
41
+ # Example:
42
+ #
43
+ # ActiveSupport::ContinuousIntegration.run do
44
+ # step "Setup", "bin/setup --skip-server"
45
+ # step "Style: Ruby", "bin/rubocop"
46
+ # step "Security: Gem audit", "bin/bundler-audit"
47
+ # step "Tests: Rails", "bin/rails test test:system"
48
+ #
49
+ # if success?
50
+ # step "Signoff: Ready for merge and deploy", "gh signoff"
51
+ # else
52
+ # failure "Skipping signoff; CI failed.", "Fix the issues and try again."
53
+ # end
54
+ # end
55
+ def self.run(title = "Continuous Integration", subtitle = "Running tests, style checks, and security audits", &block)
56
+ new.tap do |ci|
57
+ ENV["CI"] = "true"
58
+ ci.heading title, subtitle, padding: false
59
+ ci.report(title, &block)
60
+ abort unless ci.success?
61
+ end
62
+ end
63
+
64
+ def initialize
65
+ @results = []
66
+ end
67
+
68
+ # Declare a step with a title and a command. The command can either be given as a single string or as multiple
69
+ # strings that will be passed to `system` as individual arguments (and therefore correctly escaped for paths etc).
70
+ #
71
+ # Examples:
72
+ #
73
+ # step "Setup", "bin/setup"
74
+ # step "Single test", "bin/rails", "test", "--name", "test_that_is_one"
75
+ def step(title, *command)
76
+ heading title, command.join(" "), type: :title
77
+ report(title) { results << system(*command) }
78
+ end
79
+
80
+ # Returns true if all steps were successful.
81
+ def success?
82
+ results.all?
83
+ end
84
+
85
+ # Display an error heading with the title and optional subtitle to reflect that the run failed.
86
+ def failure(title, subtitle = nil)
87
+ heading title, subtitle, type: :error
88
+ end
89
+
90
+ # Display a colorized heading followed by an optional subtitle.
91
+ #
92
+ # Examples:
93
+ #
94
+ # heading "Smoke Testing", "End-to-end tests verifying key functionality", padding: false
95
+ # heading "Skipping video encoding tests", "Install FFmpeg to run these tests", type: :error
96
+ #
97
+ # See ActiveSupport::ContinuousIntegration::COLORS for a complete list of options.
98
+ def heading(heading, subtitle = nil, type: :banner, padding: true)
99
+ echo "#{padding ? "\n\n" : ""}#{heading}", type: type
100
+ echo "#{subtitle}#{padding ? "\n" : ""}", type: :subtitle if subtitle
101
+ end
102
+
103
+ # Echo text to the terminal in the color corresponding to the type of the text.
104
+ #
105
+ # Examples:
106
+ #
107
+ # echo "This is going to be green!", type: :success
108
+ # echo "This is going to be red!", type: :error
109
+ #
110
+ # See ActiveSupport::ContinuousIntegration::COLORS for a complete list of options.
111
+ def echo(text, type:)
112
+ puts colorize(text, type)
113
+ end
114
+
115
+ # :nodoc:
116
+ def report(title, &block)
117
+ Signal.trap("INT") { abort colorize(:error, "\n❌ #{title} interrupted") }
118
+
119
+ ci = self.class.new
120
+ elapsed = timing { ci.instance_eval(&block) }
121
+
122
+ if ci.success?
123
+ echo "\n✅ #{title} passed in #{elapsed}", type: :success
124
+ else
125
+ echo "\n❌ #{title} failed in #{elapsed}", type: :error
126
+ end
127
+
128
+ results.concat ci.results
129
+ ensure
130
+ Signal.trap("INT", "-")
131
+ end
132
+
133
+ private
134
+ def timing
135
+ started_at = Time.now.to_f
136
+ yield
137
+ min, sec = (Time.now.to_f - started_at).divmod(60)
138
+ "#{"#{min}m" if min > 0}%.2fs" % sec
139
+ end
140
+
141
+ def colorize(text, type)
142
+ "#{COLORS.fetch(type)}#{text}\033[0m"
143
+ end
144
+ end
145
+ end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/array/wrap"
4
- require "active_support/core_ext/array/access"
5
- require "active_support/core_ext/array/conversions"
6
- require "active_support/core_ext/array/extract"
7
- require "active_support/core_ext/array/extract_options"
8
- require "active_support/core_ext/array/grouping"
9
- require "active_support/core_ext/array/inquiry"
3
+ require_relative "array/wrap"
4
+ require_relative "array/access"
5
+ require_relative "array/conversions"
6
+ require_relative "array/extract"
7
+ require_relative "array/extract_options"
8
+ require_relative "array/grouping"
9
+ require_relative "array/inquiry"
@@ -1,13 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "benchmark"
4
-
5
- class << Benchmark
6
- def ms(&block) # :nodoc
7
- # NOTE: Please also remove the Active Support `benchmark` dependency when removing this
8
- ActiveSupport.deprecator.warn <<~TEXT
9
- `Benchmark.ms` is deprecated and will be removed in Rails 8.1 without replacement.
10
- TEXT
11
- ActiveSupport::Benchmark.realtime(:float_millisecond, &block)
12
- end
13
- end
3
+ # Remove this file from activesupport/lib/active_support/core_ext.rb when deleting the deprecation.
4
+ ActiveSupport.deprecator.warn <<~TEXT
5
+ active_support/core_ext/benchmark.rb is deprecated and will be removed in Rails 8.2 without replacement.
6
+ TEXT
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/big_decimal/conversions"
3
+ require_relative "big_decimal/conversions"
@@ -96,14 +96,16 @@ class Class
96
96
  namespaced_name = :"__class_attr_#{name}"
97
97
  ::ActiveSupport::ClassAttribute.redefine(self, name, namespaced_name, default)
98
98
 
99
- delegators = [
100
- "def #{name}; #{namespaced_name}; end",
101
- "def #{name}=(value); self.#{namespaced_name} = value; end",
102
- ]
99
+ class_methods << "def #{name}; #{namespaced_name}; end"
100
+ class_methods << "def #{name}=(value); self.#{namespaced_name} = value; end"
103
101
 
104
- class_methods.concat(delegators)
105
102
  if singleton_class?
106
- methods.concat(delegators)
103
+ methods << <<~RUBY if instance_reader
104
+ silence_redefinition_of_method(:#{name})
105
+ def #{name}
106
+ self.singleton_class.#{name}
107
+ end
108
+ RUBY
107
109
  else
108
110
  methods << <<~RUBY if instance_reader
109
111
  silence_redefinition_of_method def #{name}
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/class/attribute"
4
- require "active_support/core_ext/class/subclasses"
3
+ require_relative "class/attribute"
4
+ require_relative "class/subclasses"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/date/acts_like"
4
- require "active_support/core_ext/date/blank"
5
- require "active_support/core_ext/date/calculations"
6
- require "active_support/core_ext/date/conversions"
7
- require "active_support/core_ext/date/zones"
3
+ require_relative "date/acts_like"
4
+ require_relative "date/blank"
5
+ require_relative "date/calculations"
6
+ require_relative "date/conversions"
7
+ require_relative "date/zones"