activesupport 7.0.3.1 → 7.2.3.1

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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +261 -246
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -7
  5. data/lib/active_support/actionable_error.rb +3 -1
  6. data/lib/active_support/array_inquirer.rb +3 -1
  7. data/lib/active_support/backtrace_cleaner.rb +39 -7
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/broadcast_logger.rb +238 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +134 -0
  13. data/lib/active_support/cache/file_store.rb +51 -19
  14. data/lib/active_support/cache/mem_cache_store.rb +114 -134
  15. data/lib/active_support/cache/memory_store.rb +85 -30
  16. data/lib/active_support/cache/null_store.rb +8 -2
  17. data/lib/active_support/cache/redis_cache_store.rb +175 -154
  18. data/lib/active_support/cache/serializer_with_fallback.rb +152 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +64 -13
  20. data/lib/active_support/cache.rb +455 -378
  21. data/lib/active_support/callbacks.rb +121 -136
  22. data/lib/active_support/code_generator.rb +15 -10
  23. data/lib/active_support/concern.rb +4 -2
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/configurable.rb +10 -0
  27. data/lib/active_support/core_ext/array/conversions.rb +1 -2
  28. data/lib/active_support/core_ext/array.rb +0 -1
  29. data/lib/active_support/core_ext/benchmark.rb +1 -0
  30. data/lib/active_support/core_ext/class/attribute.rb +2 -2
  31. data/lib/active_support/core_ext/class/subclasses.rb +17 -34
  32. data/lib/active_support/core_ext/date/blank.rb +4 -0
  33. data/lib/active_support/core_ext/date/calculations.rb +15 -0
  34. data/lib/active_support/core_ext/date/conversions.rb +1 -2
  35. data/lib/active_support/core_ext/date.rb +0 -1
  36. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  37. data/lib/active_support/core_ext/date_and_time/compatibility.rb +28 -1
  38. data/lib/active_support/core_ext/date_time/blank.rb +4 -0
  39. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  40. data/lib/active_support/core_ext/date_time/conversions.rb +6 -4
  41. data/lib/active_support/core_ext/date_time.rb +0 -1
  42. data/lib/active_support/core_ext/digest/uuid.rb +7 -10
  43. data/lib/active_support/core_ext/enumerable.rb +25 -80
  44. data/lib/active_support/core_ext/erb/util.rb +201 -0
  45. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  46. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  47. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  48. data/lib/active_support/core_ext/hash/keys.rb +7 -7
  49. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  50. data/lib/active_support/core_ext/module/attr_internal.rb +17 -6
  51. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  52. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  53. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  54. data/lib/active_support/core_ext/module/delegation.rb +20 -119
  55. data/lib/active_support/core_ext/module/deprecation.rb +12 -12
  56. data/lib/active_support/core_ext/module/introspection.rb +3 -1
  57. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  58. data/lib/active_support/core_ext/numeric/conversions.rb +5 -3
  59. data/lib/active_support/core_ext/numeric.rb +0 -1
  60. data/lib/active_support/core_ext/object/blank.rb +45 -1
  61. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  62. data/lib/active_support/core_ext/object/duplicable.rb +25 -16
  63. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  64. data/lib/active_support/core_ext/object/instance_variables.rb +4 -2
  65. data/lib/active_support/core_ext/object/json.rb +17 -7
  66. data/lib/active_support/core_ext/object/to_query.rb +0 -2
  67. data/lib/active_support/core_ext/object/try.rb +2 -2
  68. data/lib/active_support/core_ext/object/with.rb +46 -0
  69. data/lib/active_support/core_ext/object/with_options.rb +9 -9
  70. data/lib/active_support/core_ext/object.rb +1 -0
  71. data/lib/active_support/core_ext/pathname/blank.rb +20 -0
  72. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  73. data/lib/active_support/core_ext/pathname.rb +1 -0
  74. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  75. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  76. data/lib/active_support/core_ext/range/sole.rb +17 -0
  77. data/lib/active_support/core_ext/range.rb +2 -2
  78. data/lib/active_support/core_ext/securerandom.rb +24 -12
  79. data/lib/active_support/core_ext/string/conversions.rb +1 -1
  80. data/lib/active_support/core_ext/string/filters.rb +24 -18
  81. data/lib/active_support/core_ext/string/indent.rb +1 -1
  82. data/lib/active_support/core_ext/string/inflections.rb +16 -9
  83. data/lib/active_support/core_ext/string/multibyte.rb +3 -3
  84. data/lib/active_support/core_ext/string/output_safety.rb +41 -178
  85. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  86. data/lib/active_support/core_ext/time/calculations.rb +40 -30
  87. data/lib/active_support/core_ext/time/compatibility.rb +24 -0
  88. data/lib/active_support/core_ext/time/conversions.rb +1 -3
  89. data/lib/active_support/core_ext/time/zones.rb +7 -8
  90. data/lib/active_support/core_ext/time.rb +0 -1
  91. data/lib/active_support/core_ext.rb +0 -1
  92. data/lib/active_support/current_attributes.rb +60 -46
  93. data/lib/active_support/deep_mergeable.rb +53 -0
  94. data/lib/active_support/delegation.rb +202 -0
  95. data/lib/active_support/dependencies/autoload.rb +9 -16
  96. data/lib/active_support/deprecation/behaviors.rb +64 -41
  97. data/lib/active_support/deprecation/constant_accessor.rb +47 -25
  98. data/lib/active_support/deprecation/deprecators.rb +104 -0
  99. data/lib/active_support/deprecation/disallowed.rb +6 -8
  100. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  101. data/lib/active_support/deprecation/proxy_wrappers.rb +34 -22
  102. data/lib/active_support/deprecation/reporting.rb +49 -27
  103. data/lib/active_support/deprecation.rb +39 -9
  104. data/lib/active_support/deprecator.rb +7 -0
  105. data/lib/active_support/descendants_tracker.rb +66 -172
  106. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  107. data/lib/active_support/duration/iso8601_serializer.rb +1 -4
  108. data/lib/active_support/duration.rb +13 -7
  109. data/lib/active_support/encrypted_configuration.rb +63 -11
  110. data/lib/active_support/encrypted_file.rb +29 -13
  111. data/lib/active_support/environment_inquirer.rb +22 -2
  112. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  113. data/lib/active_support/error_reporter.rb +163 -36
  114. data/lib/active_support/evented_file_update_checker.rb +17 -3
  115. data/lib/active_support/execution_wrapper.rb +5 -6
  116. data/lib/active_support/file_update_checker.rb +6 -4
  117. data/lib/active_support/fork_tracker.rb +4 -32
  118. data/lib/active_support/gem_version.rb +2 -2
  119. data/lib/active_support/gzip.rb +2 -0
  120. data/lib/active_support/hash_with_indifferent_access.rb +50 -30
  121. data/lib/active_support/html_safe_translation.rb +19 -6
  122. data/lib/active_support/i18n.rb +1 -1
  123. data/lib/active_support/i18n_railtie.rb +20 -13
  124. data/lib/active_support/inflector/inflections.rb +2 -0
  125. data/lib/active_support/inflector/methods.rb +28 -18
  126. data/lib/active_support/inflector/transliterate.rb +3 -1
  127. data/lib/active_support/isolated_execution_state.rb +26 -22
  128. data/lib/active_support/json/decoding.rb +3 -2
  129. data/lib/active_support/json/encoding.rb +48 -48
  130. data/lib/active_support/key_generator.rb +9 -1
  131. data/lib/active_support/lazy_load_hooks.rb +22 -7
  132. data/lib/active_support/locale/en.yml +2 -0
  133. data/lib/active_support/log_subscriber.rb +74 -34
  134. data/lib/active_support/logger.rb +22 -59
  135. data/lib/active_support/logger_thread_safe_level.rb +10 -32
  136. data/lib/active_support/message_encryptor.rb +197 -53
  137. data/lib/active_support/message_encryptors.rb +141 -0
  138. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  139. data/lib/active_support/message_pack/extensions.rb +305 -0
  140. data/lib/active_support/message_pack/serializer.rb +63 -0
  141. data/lib/active_support/message_pack.rb +50 -0
  142. data/lib/active_support/message_verifier.rb +229 -89
  143. data/lib/active_support/message_verifiers.rb +137 -0
  144. data/lib/active_support/messages/codec.rb +65 -0
  145. data/lib/active_support/messages/metadata.rb +111 -45
  146. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  147. data/lib/active_support/messages/rotator.rb +38 -31
  148. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  149. data/lib/active_support/multibyte/chars.rb +8 -3
  150. data/lib/active_support/multibyte/unicode.rb +9 -37
  151. data/lib/active_support/notifications/fanout.rb +248 -87
  152. data/lib/active_support/notifications/instrumenter.rb +93 -25
  153. data/lib/active_support/notifications.rb +36 -29
  154. data/lib/active_support/number_helper/number_converter.rb +16 -7
  155. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  156. data/lib/active_support/number_helper/number_to_delimited_converter.rb +17 -2
  157. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  158. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  159. data/lib/active_support/number_helper.rb +379 -317
  160. data/lib/active_support/option_merger.rb +4 -4
  161. data/lib/active_support/ordered_hash.rb +3 -3
  162. data/lib/active_support/ordered_options.rb +67 -15
  163. data/lib/active_support/parameter_filter.rb +103 -84
  164. data/lib/active_support/proxy_object.rb +8 -3
  165. data/lib/active_support/railtie.rb +25 -20
  166. data/lib/active_support/reloader.rb +12 -4
  167. data/lib/active_support/rescuable.rb +10 -8
  168. data/lib/active_support/secure_compare_rotator.rb +16 -9
  169. data/lib/active_support/string_inquirer.rb +4 -2
  170. data/lib/active_support/subscriber.rb +10 -27
  171. data/lib/active_support/syntax_error_proxy.rb +60 -0
  172. data/lib/active_support/tagged_logging.rb +64 -40
  173. data/lib/active_support/test_case.rb +160 -7
  174. data/lib/active_support/testing/assertions.rb +28 -12
  175. data/lib/active_support/testing/autorun.rb +0 -2
  176. data/lib/active_support/testing/constant_stubbing.rb +54 -0
  177. data/lib/active_support/testing/deprecation.rb +20 -27
  178. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  179. data/lib/active_support/testing/isolation.rb +46 -33
  180. data/lib/active_support/testing/method_call_assertions.rb +7 -8
  181. data/lib/active_support/testing/parallelization/server.rb +18 -2
  182. data/lib/active_support/testing/parallelization/worker.rb +2 -2
  183. data/lib/active_support/testing/parallelization.rb +12 -1
  184. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  185. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  186. data/lib/active_support/testing/stream.rb +1 -1
  187. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  188. data/lib/active_support/testing/time_helpers.rb +38 -16
  189. data/lib/active_support/time_with_zone.rb +18 -44
  190. data/lib/active_support/values/time_zone.rb +25 -14
  191. data/lib/active_support/version.rb +1 -1
  192. data/lib/active_support/xml_mini/jdom.rb +3 -10
  193. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  194. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  195. data/lib/active_support/xml_mini/rexml.rb +1 -1
  196. data/lib/active_support/xml_mini.rb +14 -3
  197. data/lib/active_support.rb +15 -3
  198. metadata +148 -24
  199. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  200. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -26
  201. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -22
  202. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  203. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -26
  204. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -7
  205. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  206. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -22
  207. data/lib/active_support/core_ext/uri.rb +0 -5
  208. data/lib/active_support/deprecation/instance_delegator.rb +0 -38
  209. data/lib/active_support/per_thread_registry.rb +0 -65
  210. data/lib/active_support/ruby_features.rb +0 -7
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module Testing
5
+ module ConstantStubbing
6
+ # Changes the value of a constant for the duration of a block. Example:
7
+ #
8
+ # # World::List::Import::LARGE_IMPORT_THRESHOLD = 5000
9
+ # stub_const(World::List::Import, :LARGE_IMPORT_THRESHOLD, 1) do
10
+ # assert_equal 1, World::List::Import::LARGE_IMPORT_THRESHOLD
11
+ # end
12
+ #
13
+ # assert_equal 5000, World::List::Import::LARGE_IMPORT_THRESHOLD
14
+ #
15
+ # Using this method rather than forcing <tt>World::List::Import::LARGE_IMPORT_THRESHOLD = 5000</tt> prevents
16
+ # warnings from being thrown, and ensures that the old value is returned after the test has completed.
17
+ #
18
+ # If the constant doesn't already exists, but you need it set for the duration of the block
19
+ # you can do so by passing `exists: false`.
20
+ #
21
+ # stub_const(object, :SOME_CONST, 1, exists: false) do
22
+ # assert_equal 1, SOME_CONST
23
+ # end
24
+ #
25
+ # Note: Stubbing a const will stub it across all threads. So if you have concurrent threads
26
+ # (like separate test suites running in parallel) that all depend on the same constant, it's possible
27
+ # divergent stubbing will trample on each other.
28
+ def stub_const(mod, constant, new_value, exists: true)
29
+ if exists
30
+ begin
31
+ old_value = mod.const_get(constant, false)
32
+ mod.send(:remove_const, constant)
33
+ mod.const_set(constant, new_value)
34
+ yield
35
+ ensure
36
+ mod.send(:remove_const, constant)
37
+ mod.const_set(constant, old_value)
38
+ end
39
+ else
40
+ if mod.const_defined?(constant)
41
+ raise NameError, "already defined constant #{constant} in #{mod.name}"
42
+ end
43
+
44
+ begin
45
+ mod.const_set(constant, new_value)
46
+ yield
47
+ ensure
48
+ mod.send(:remove_const, constant)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -5,6 +5,11 @@ require "active_support/deprecation"
5
5
  module ActiveSupport
6
6
  module Testing
7
7
  module Deprecation
8
+ ##
9
+ # :call-seq:
10
+ # assert_deprecated(deprecator, &block)
11
+ # assert_deprecated(match, deprecator, &block)
12
+ #
8
13
  # Asserts that a matching deprecation warning was emitted by the given deprecator during the execution of the yielded block.
9
14
  #
10
15
  # assert_deprecated(/foo/, CustomDeprecator) do
@@ -19,16 +24,16 @@ module ActiveSupport
19
24
  #
20
25
  # If the +match+ is omitted (or explicitly +nil+), any deprecation warning will match.
21
26
  #
22
- # assert_deprecated(nil, CustomDeprecator) do
27
+ # assert_deprecated(CustomDeprecator) do
23
28
  # CustomDeprecator.warn "foo should no longer be used"
24
29
  # end
25
- #
26
- # If no +deprecator+ is given, defaults to ActiveSupport::Deprecation.
27
- #
28
- # assert_deprecated do
29
- # ActiveSupport::Deprecation.warn "foo should no longer be used"
30
- # end
31
30
  def assert_deprecated(match = nil, deprecator = nil, &block)
31
+ match, deprecator = nil, match if match.is_a?(ActiveSupport::Deprecation)
32
+
33
+ unless deprecator
34
+ raise ArgumentError, "No deprecator given"
35
+ end
36
+
32
37
  result, warnings = collect_deprecations(deprecator, &block)
33
38
  assert !warnings.empty?, "Expected a deprecation warning within the block but received none"
34
39
  if match
@@ -44,36 +49,24 @@ module ActiveSupport
44
49
  # CustomDeprecator.warn "message" # fails assertion
45
50
  # end
46
51
  #
47
- # If no +deprecator+ is given, defaults to ActiveSupport::Deprecation.
48
- #
49
- # assert_not_deprecated do
50
- # ActiveSupport::Deprecation.warn "message" # fails assertion
51
- # end
52
- #
53
- # assert_not_deprecated do
54
- # CustomDeprecator.warn "message" # passes assertion
52
+ # assert_not_deprecated(ActiveSupport::Deprecation.new) do
53
+ # CustomDeprecator.warn "message" # passes assertion, different deprecator
55
54
  # end
56
- def assert_not_deprecated(deprecator = nil, &block)
55
+ def assert_not_deprecated(deprecator, &block)
57
56
  result, deprecations = collect_deprecations(deprecator, &block)
58
57
  assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}"
59
58
  result
60
59
  end
61
60
 
62
- # Returns an array of all the deprecation warnings emitted by the given
61
+ # Returns the return value of the block and an array of all the deprecation warnings emitted by the given
63
62
  # +deprecator+ during the execution of the yielded block.
64
63
  #
65
64
  # collect_deprecations(CustomDeprecator) do
66
65
  # CustomDeprecator.warn "message"
67
- # end # => ["message"]
68
- #
69
- # If no +deprecator+ is given, defaults to ActiveSupport::Deprecation.
70
- #
71
- # collect_deprecations do
72
- # CustomDeprecator.warn "custom message"
73
- # ActiveSupport::Deprecation.warn "message"
74
- # end # => ["message"]
75
- def collect_deprecations(deprecator = nil)
76
- deprecator ||= ActiveSupport::Deprecation
66
+ # ActiveSupport::Deprecation.new.warn "other message"
67
+ # :result
68
+ # end # => [:result, ["message"]]
69
+ def collect_deprecations(deprecator)
77
70
  old_behavior = deprecator.behavior
78
71
  deprecations = []
79
72
  deprecator.behavior = Proc.new do |message, callstack|
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module Testing
5
+ module ErrorReporterAssertions
6
+ module ErrorCollector # :nodoc:
7
+ @subscribed = false
8
+ @mutex = Mutex.new
9
+
10
+ Report = Struct.new(:error, :handled, :severity, :context, :source, keyword_init: true)
11
+ class Report
12
+ alias_method :handled?, :handled
13
+ end
14
+
15
+ class << self
16
+ def record
17
+ subscribe
18
+ recorders = ActiveSupport::IsolatedExecutionState[:active_support_error_reporter_assertions] ||= []
19
+ reports = []
20
+ recorders << reports
21
+ begin
22
+ yield
23
+ reports
24
+ ensure
25
+ recorders.delete_if { |r| reports.equal?(r) }
26
+ end
27
+ end
28
+
29
+ def report(error, **kwargs)
30
+ report = Report.new(error: error, **kwargs)
31
+ ActiveSupport::IsolatedExecutionState[:active_support_error_reporter_assertions]&.each do |reports|
32
+ reports << report
33
+ end
34
+ true
35
+ end
36
+
37
+ private
38
+ def subscribe
39
+ return if @subscribed
40
+ @mutex.synchronize do
41
+ return if @subscribed
42
+
43
+ if ActiveSupport.error_reporter
44
+ ActiveSupport.error_reporter.subscribe(self)
45
+ @subscribed = true
46
+ else
47
+ raise Minitest::Assertion, "No error reporter is configured"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ # Assertion that the block should not cause an exception to be reported
55
+ # to +Rails.error+.
56
+ #
57
+ # Passes if evaluated code in the yielded block reports no exception.
58
+ #
59
+ # assert_no_error_reported do
60
+ # perform_service(param: 'no_exception')
61
+ # end
62
+ def assert_no_error_reported(&block)
63
+ reports = ErrorCollector.record do
64
+ _assert_nothing_raised_or_warn("assert_no_error_reported", &block)
65
+ end
66
+ assert_predicate(reports, :empty?)
67
+ end
68
+
69
+ # Assertion that the block should cause at least one exception to be reported
70
+ # to +Rails.error+.
71
+ #
72
+ # Passes if the evaluated code in the yielded block reports a matching exception.
73
+ #
74
+ # assert_error_reported(IOError) do
75
+ # Rails.error.report(IOError.new("Oops"))
76
+ # end
77
+ #
78
+ # To test further details about the reported exception, you can use the return
79
+ # value.
80
+ #
81
+ # report = assert_error_reported(IOError) do
82
+ # # ...
83
+ # end
84
+ # assert_equal "Oops", report.error.message
85
+ # assert_equal "admin", report.context[:section]
86
+ # assert_equal :warning, report.severity
87
+ # assert_predicate report, :handled?
88
+ def assert_error_reported(error_class = StandardError, &block)
89
+ reports = ErrorCollector.record do
90
+ _assert_nothing_raised_or_warn("assert_error_reported", &block)
91
+ end
92
+
93
+ if reports.empty?
94
+ assert(false, "Expected a #{error_class.name} to be reported, but there were no errors reported.")
95
+ elsif (report = reports.find { |r| error_class === r.error })
96
+ self.assertions += 1
97
+ report
98
+ else
99
+ message = "Expected a #{error_class.name} to be reported, but none of the " \
100
+ "#{reports.size} reported errors matched: \n" \
101
+ "#{reports.map { |r| r.error.class.name }.join("\n ")}"
102
+ assert(false, message)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -1,13 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/testing/parallelize_executor"
4
+
3
5
  module ActiveSupport
4
6
  module Testing
5
7
  module Isolation
6
8
  require "thread"
7
9
 
10
+ SubprocessCrashed = Class.new(StandardError)
11
+
8
12
  def self.included(klass) # :nodoc:
9
13
  klass.class_eval do
10
- parallelize_me!
14
+ parallelize_me! unless Minitest.parallel_executor.is_a?(ActiveSupport::Testing::ParallelizeExecutor)
11
15
  end
12
16
  end
13
17
 
@@ -16,54 +20,62 @@ module ActiveSupport
16
20
  end
17
21
 
18
22
  def run
19
- serialized = run_in_isolation do
23
+ status, serialized = run_in_isolation do
20
24
  super
21
25
  end
22
26
 
27
+ unless status&.success?
28
+ error = SubprocessCrashed.new("Subprocess exited with an error: #{status.inspect}\noutput: #{serialized.inspect}")
29
+ error.set_backtrace(caller)
30
+ self.failures << Minitest::UnexpectedError.new(error)
31
+ return defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
32
+ end
33
+
23
34
  Marshal.load(serialized)
24
35
  end
25
36
 
26
37
  module Forking
27
38
  def run_in_isolation(&blk)
28
- read, write = IO.pipe
29
- read.binmode
30
- write.binmode
39
+ IO.pipe do |read, write|
40
+ read.binmode
41
+ write.binmode
31
42
 
32
- pid = fork do
33
- read.close
34
- yield
35
- begin
36
- if error?
37
- failures.map! { |e|
38
- begin
39
- Marshal.dump e
40
- e
41
- rescue TypeError
42
- ex = Exception.new e.message
43
- ex.set_backtrace e.backtrace
44
- Minitest::UnexpectedError.new ex
45
- end
46
- }
43
+ pid = fork do
44
+ read.close
45
+ yield
46
+ begin
47
+ if error?
48
+ failures.map! { |e|
49
+ begin
50
+ Marshal.dump e
51
+ e
52
+ rescue TypeError
53
+ ex = Exception.new e.message
54
+ ex.set_backtrace e.backtrace
55
+ Minitest::UnexpectedError.new ex
56
+ end
57
+ }
58
+ end
59
+ test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
60
+ result = Marshal.dump(test_result)
47
61
  end
48
- test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
49
- result = Marshal.dump(test_result)
62
+
63
+ write.puts [result].pack("m")
64
+ exit!(0)
50
65
  end
51
66
 
52
- write.puts [result].pack("m")
53
- exit!
67
+ write.close
68
+ result = read.read
69
+ _, status = Process.wait2(pid)
70
+ return status, result.unpack1("m")
54
71
  end
55
-
56
- write.close
57
- result = read.read
58
- Process.wait2(pid)
59
- result.unpack1("m")
60
72
  end
61
73
  end
62
74
 
63
75
  module Subprocess
64
76
  ORIG_ARGV = ARGV.dup unless defined?(ORIG_ARGV)
65
77
 
66
- # Complicated H4X to get this working in windows / jruby with
78
+ # Complicated H4X to get this working in Windows / JRuby with
67
79
  # no forking.
68
80
  def run_in_isolation(&blk)
69
81
  require "tempfile"
@@ -74,7 +86,7 @@ module ActiveSupport
74
86
  File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
75
87
  file.puts [Marshal.dump(test_result)].pack("m")
76
88
  end
77
- exit!
89
+ exit!(0)
78
90
  else
79
91
  Tempfile.open("isolation") do |tmpfile|
80
92
  env = {
@@ -92,13 +104,14 @@ module ActiveSupport
92
104
 
93
105
  child = IO.popen([env, Gem.ruby, *load_path_args, $0, *ORIG_ARGV, test_opts])
94
106
 
107
+ status = nil
95
108
  begin
96
- Process.wait(child.pid)
109
+ _, status = Process.wait2(child.pid)
97
110
  rescue Errno::ECHILD # The child process may exit before we wait
98
111
  nil
99
112
  end
100
113
 
101
- return tmpfile.read.unpack1("m")
114
+ return status, tmpfile.read.unpack1("m")
102
115
  end
103
116
  end
104
117
  end
@@ -17,24 +17,23 @@ module ActiveSupport
17
17
  assert_equal times, times_called, error
18
18
  end
19
19
 
20
- def assert_called_with(object, method_name, args, returns: nil, &block)
20
+ def assert_called_with(object, method_name, args, returns: false, **kwargs, &block)
21
21
  mock = Minitest::Mock.new
22
-
23
- if args.all?(Array)
24
- args.each { |arg| mock.expect(:call, returns, arg) }
25
- else
26
- mock.expect(:call, returns, args)
27
- end
22
+ expect_called_with(mock, args, returns: returns, **kwargs)
28
23
 
29
24
  object.stub(method_name, mock, &block)
30
25
 
31
- mock.verify
26
+ assert_mock(mock)
32
27
  end
33
28
 
34
29
  def assert_not_called(object, method_name, message = nil, &block)
35
30
  assert_called(object, method_name, message, times: 0, &block)
36
31
  end
37
32
 
33
+ def expect_called_with(mock, args, returns: false, **kwargs)
34
+ mock.expect(:call, returns, args, **kwargs)
35
+ end
36
+
38
37
  def assert_called_on_instance_of(klass, method_name, message = nil, times: 1, returns: nil)
39
38
  times_called = 0
40
39
  klass.define_method("stubbed_#{method_name}") do |*|
@@ -6,12 +6,15 @@ require "drb/unix" unless Gem.win_platform?
6
6
  module ActiveSupport
7
7
  module Testing
8
8
  class Parallelization # :nodoc:
9
+ PrerecordResultClass = Struct.new(:name)
10
+
9
11
  class Server
10
12
  include DRb::DRbUndumped
11
13
 
12
14
  def initialize
13
15
  @queue = Queue.new
14
16
  @active_workers = Concurrent::Map.new
17
+ @worker_pids = Concurrent::Map.new
15
18
  @in_flight = Concurrent::Map.new
16
19
  end
17
20
 
@@ -21,6 +24,7 @@ module ActiveSupport
21
24
  @in_flight.delete([result.klass, result.name])
22
25
 
23
26
  reporter.synchronize do
27
+ reporter.prerecord(PrerecordResultClass.new(result.klass), result.name)
24
28
  reporter.record(result)
25
29
  end
26
30
  end
@@ -37,12 +41,24 @@ module ActiveSupport
37
41
  end
38
42
  end
39
43
 
40
- def start_worker(worker_id)
44
+ def start_worker(worker_id, worker_pid)
41
45
  @active_workers[worker_id] = true
46
+ @worker_pids[worker_id] = worker_pid
42
47
  end
43
48
 
44
- def stop_worker(worker_id)
49
+ def stop_worker(worker_id, worker_pid)
45
50
  @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
46
62
  end
47
63
 
48
64
  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)
21
+ @queue.start_worker(@id, Process.pid)
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)
32
+ @queue.stop_worker(@id, Process.pid)
33
33
  end
34
34
  end
35
35
 
@@ -47,8 +47,19 @@ module ActiveSupport
47
47
  end
48
48
 
49
49
  def shutdown
50
+ dead_worker_pids = @worker_pool.filter_map do |pid|
51
+ Process.waitpid(pid, Process::WNOHANG)
52
+ rescue Errno::ECHILD
53
+ pid
54
+ end
55
+ @queue_server.remove_dead_workers(dead_worker_pids)
56
+
50
57
  @queue_server.shutdown
51
- @worker_pool.each { |pid| Process.waitpid pid }
58
+ @worker_pool.each do |pid|
59
+ Process.waitpid(pid)
60
+ rescue Errno::ECHILD
61
+ nil
62
+ end
52
63
  end
53
64
  end
54
65
  end
@@ -9,6 +9,7 @@ module ActiveSupport
9
9
  @size = size
10
10
  @parallelize_with = with
11
11
  @threshold = threshold
12
+ @parallelized = false
12
13
  end
13
14
 
14
15
  def start
@@ -49,11 +50,15 @@ module ActiveSupport
49
50
  end
50
51
 
51
52
  def parallelized?
52
- @parallelized if defined?(@parallelized)
53
+ @parallelized
53
54
  end
54
55
 
55
56
  def should_parallelize?
56
- ENV["PARALLEL_WORKERS"] || tests_count > threshold
57
+ (ENV["PARALLEL_WORKERS"] || tests_count > threshold) && many_workers?
58
+ end
59
+
60
+ def many_workers?
61
+ size > 1
57
62
  end
58
63
 
59
64
  def tests_count
@@ -67,7 +72,7 @@ module ActiveSupport
67
72
  def execution_info
68
73
  if parallelized?
69
74
  "Running #{tests_count} tests in parallel using #{parallel_executor.size} #{parallelize_with}"
70
- else
75
+ elsif many_workers?
71
76
  "Running #{tests_count} tests in a single process (parallelization threshold is #{threshold})"
72
77
  end
73
78
  end
@@ -46,6 +46,8 @@ module ActiveSupport
46
46
  run_callbacks :teardown
47
47
  rescue => e
48
48
  self.failures << Minitest::UnexpectedError.new(e)
49
+ rescue Minitest::Assertion => e
50
+ self.failures << e
49
51
  end
50
52
 
51
53
  super
@@ -23,7 +23,7 @@ module ActiveSupport
23
23
  def capture(stream)
24
24
  stream = stream.to_s
25
25
  captured_stream = Tempfile.new(stream)
26
- stream_io = eval("$#{stream}")
26
+ stream_io = eval("$#{stream}", binding, __FILE__, __LINE__)
27
27
  origin_stream = stream_io.dup
28
28
  stream_io.reopen(captured_stream)
29
29
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module Testing
5
+ # Warns when a test case does not perform any assertions.
6
+ #
7
+ # This is helpful in detecting broken tests that do not perform intended assertions.
8
+ module TestsWithoutAssertions # :nodoc:
9
+ def after_teardown
10
+ super
11
+
12
+ if assertions.zero? && !skipped? && !error?
13
+ file, line = method(name).source_location
14
+ warn "Test is missing assertions: `#{name}` #{file}:#{line}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end