Skip to content

Commit 99f918b

Browse files
authored
Merge pull request rails#55247 from byroot/nested-state
Allow for nested ExecutionContext in test
2 parents a555fc8 + d43dc9a commit 99f918b

File tree

6 files changed

+150
-25
lines changed

6 files changed

+150
-25
lines changed

activestorage/test/test_helper.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
require "active_support/testing/autorun"
1313
require "image_processing/mini_magick"
1414

15+
require "active_support/current_attributes/test_helper"
1516
require "active_record/testing/query_assertions"
1617

1718
require "active_job"
@@ -25,6 +26,7 @@
2526
class ActiveSupport::TestCase
2627
self.file_fixture_path = ActiveStorage::FixtureSet.file_fixture_path
2728

29+
include ActiveSupport::CurrentAttributes::TestHelper
2830
include ActiveRecord::TestFixtures
2931
include ActiveRecord::Assertions::QueryAssertions
3032

@@ -34,10 +36,6 @@ class ActiveSupport::TestCase
3436
ActiveStorage::Current.url_options = { protocol: "https://", host: "example.com", port: nil }
3537
end
3638

37-
teardown do
38-
ActiveStorage::Current.reset
39-
end
40-
4139
private
4240
def create_blob(key: nil, data: "Hello world!", filename: "hello.txt", content_type: "text/plain", identify: true, service_name: nil, record: nil)
4341
ActiveStorage::Blob.create_and_upload! key: key, io: StringIO.new(data), filename: filename, content_type: content_type, identify: identify, service_name: service_name, record: record

activesupport/CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
* Improve `CurrentAttribute` and `ExecutionContext` state managment in test cases.
2+
3+
Previously these two global state would be entirely cleared out whenever calling
4+
into code that is wrapped by the Rails executor, typically Action Controller or
5+
Active Job helpers:
6+
7+
```ruby
8+
test "#index works" do
9+
CurrentUser.id = 42
10+
get :index
11+
CurrentUser.id == nil
12+
end
13+
```
14+
15+
Now re-entering the executor properly save and restore that state.
16+
17+
*Jean Boussier*
18+
119
* The new method `ActiveSupport::BacktraceCleaner#first_clean_location`
220
returns the first clean location of the caller's call stack, or `nil`.
321
Locations are `Thread::Backtrace::Location` objects. Useful when you want to

activesupport/lib/active_support/current_attributes.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "active_support/callbacks"
4+
require "active_support/execution_context"
45
require "active_support/core_ext/object/with"
56
require "active_support/core_ext/enumerable"
67
require "active_support/core_ext/module/delegation"
@@ -154,8 +155,10 @@ def resets(*methods, &block)
154155
delegate :set, :reset, to: :instance
155156

156157
def clear_all # :nodoc:
157-
current_instances.each_value(&:reset)
158-
current_instances.clear
158+
if instances = current_instances
159+
instances.values.each(&:reset)
160+
instances.clear
161+
end
159162
end
160163

161164
private
@@ -164,7 +167,7 @@ def generated_attribute_methods
164167
end
165168

166169
def current_instances
167-
IsolatedExecutionState[:current_attributes_instances] ||= {}
170+
ExecutionContext.current_attributes_instances
168171
end
169172

170173
def current_instances_key

activesupport/lib/active_support/execution_context.rb

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,41 @@
22

33
module ActiveSupport
44
module ExecutionContext # :nodoc:
5+
class Record
6+
attr_reader :store, :current_attributes_instances
7+
8+
def initialize
9+
@store = {}
10+
@current_attributes_instances = {}
11+
@stack = []
12+
end
13+
14+
def push
15+
@stack << @store << @current_attributes_instances
16+
@store = {}
17+
@current_attributes_instances = {}
18+
self
19+
end
20+
21+
def pop
22+
@current_attributes_instances = @stack.pop
23+
@store = @stack.pop
24+
self
25+
end
26+
end
27+
528
@after_change_callbacks = []
29+
30+
# Execution context nesting should only legitimately happen during test
31+
# because the test case itself is wrapped in an executor, and it might call
32+
# into a controller or job which should be executed with their own fresh context.
33+
# However in production this should never happen, and for extra safety we make sure to
34+
# fully clear the state at the end of the request or job cycle.
35+
@nestable = false
36+
637
class << self
38+
attr_accessor :nestable
39+
740
def after_change(&block)
841
@after_change_callbacks << block
942
end
@@ -14,9 +47,11 @@ def set(**options)
1447
options.symbolize_keys!
1548
keys = options.keys
1649

17-
store = self.store
50+
store = record.store
1851

19-
previous_context = keys.zip(store.values_at(*keys)).to_h
52+
previous_context = if block_given?
53+
keys.zip(store.values_at(*keys)).to_h
54+
end
2055

2156
store.merge!(options)
2257
@after_change_callbacks.each(&:call)
@@ -32,21 +67,43 @@ def set(**options)
3267
end
3368

3469
def []=(key, value)
35-
store[key.to_sym] = value
70+
record.store[key.to_sym] = value
3671
@after_change_callbacks.each(&:call)
3772
end
3873

3974
def to_h
40-
store.dup
75+
record.store.dup
76+
end
77+
78+
def push
79+
if @nestable
80+
record.push
81+
else
82+
clear
83+
end
84+
self
85+
end
86+
87+
def pop
88+
if @nestable
89+
record.pop
90+
else
91+
clear
92+
end
93+
self
4194
end
4295

4396
def clear
44-
store.clear
97+
IsolatedExecutionState[:active_support_execution_context] = nil
98+
end
99+
100+
def current_attributes_instances
101+
record.current_attributes_instances
45102
end
46103

47104
private
48-
def store
49-
IsolatedExecutionState[:active_support_execution_context] ||= {}
105+
def record
106+
IsolatedExecutionState[:active_support_execution_context] ||= Record.new
50107
end
51108
end
52109
end

activesupport/lib/active_support/railtie.rb

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,24 @@ class Railtie < Rails::Railtie # :nodoc:
3939
end
4040

4141
initializer "active_support.reset_execution_context" do |app|
42-
app.reloader.before_class_unload { ActiveSupport::ExecutionContext.clear }
43-
app.executor.to_run { ActiveSupport::ExecutionContext.clear }
44-
app.executor.to_complete { ActiveSupport::ExecutionContext.clear }
45-
end
42+
app.reloader.before_class_unload do
43+
ActiveSupport::CurrentAttributes.clear_all
44+
ActiveSupport::ExecutionContext.clear
45+
end
46+
47+
app.executor.to_run do
48+
ActiveSupport::ExecutionContext.push
49+
end
4650

47-
initializer "active_support.clear_all_current_attributes_instances" do |app|
48-
app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all }
49-
app.executor.to_run { ActiveSupport::CurrentAttributes.clear_all }
50-
app.executor.to_complete { ActiveSupport::CurrentAttributes.clear_all }
51+
app.executor.to_complete do
52+
ActiveSupport::CurrentAttributes.clear_all
53+
ActiveSupport::ExecutionContext.pop
54+
end
5155

5256
ActiveSupport.on_load(:active_support_test_case) do
5357
if app.config.active_support.executor_around_test_case
58+
ActiveSupport::ExecutionContext.nestable = true
59+
5460
require "active_support/executor/test_helper"
5561
include ActiveSupport::Executor::TestHelper
5662
else

activesupport/test/current_attributes_test.rb

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,6 @@ class Session < ActiveSupport::CurrentAttributes
6666
attribute :current, :previous
6767
end
6868

69-
# Eagerly set-up `instance`s by reference.
70-
[ Current.instance, Session.instance ]
71-
7269
# Use library specific minitest hook to catch Time.zone before reset is called via TestHelper
7370
def before_setup
7471
@original_time_zone = Time.zone
@@ -307,4 +304,50 @@ def self.name
307304
assert_equal [], current.method(:attr).parameters
308305
assert_equal [[:req, :value]], current.method(:attr=).parameters
309306
end
307+
308+
309+
test "set and restore attributes when re-entering the executor" do
310+
ActiveSupport::ExecutionContext.with(nestable: true) do
311+
# simulate executor hooks from active_support/railtie.rb
312+
executor = Class.new(ActiveSupport::Executor)
313+
executor.to_run do
314+
ActiveSupport::ExecutionContext.push
315+
end
316+
317+
executor.to_complete do
318+
ActiveSupport::CurrentAttributes.clear_all
319+
ActiveSupport::ExecutionContext.pop
320+
end
321+
322+
Current.world = "world/1"
323+
Current.account = "account/1"
324+
325+
assert_equal "world/1", Current.world
326+
assert_equal "account/1", Current.account
327+
328+
Current.set(world: "world/2", account: "account/2") do
329+
assert_equal "world/2", Current.world
330+
assert_equal "account/2", Current.account
331+
332+
executor.wrap do
333+
assert_nil Current.world
334+
assert_nil Current.account
335+
336+
Current.world = "world/3"
337+
Current.account = "account/3"
338+
339+
assert_equal "world/3", Current.world
340+
assert_equal "account/3", Current.account
341+
342+
ActiveSupport::CurrentAttributes.clear_all
343+
344+
assert_nil Current.world
345+
assert_nil Current.account
346+
end
347+
end
348+
349+
assert_equal "world/1", Current.world
350+
assert_equal "account/1", Current.account
351+
end
352+
end
310353
end

0 commit comments

Comments
 (0)