Skip to content

Commit 5a58870

Browse files
authored
Merge pull request #2728 from mhenrixon/register-compile-cache-hook-only-without-template-caching
Only register the compile cache executor hook without template caching
2 parents 5713b34 + 6af7b96 commit 5a58870

6 files changed

Lines changed: 109 additions & 33 deletions

File tree

‎Rakefile‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ RSpec::Core::RakeTask.new(:spec)
1414
Rake::TestTask.new(:test) do |t|
1515
t.libs << "test"
1616
t.libs << "lib"
17-
t.test_files = FileList["test/**/*_test.rb"]
17+
t.test_files = FileList["test/**/*_test.rb"].exclude("test/isolated/**/*")
18+
end
19+
20+
# Each test boots its own application, so these can't share a process with the sandbox.
21+
Rake::TestTask.new(:isolated_test) do |t|
22+
t.libs << "lib"
23+
t.test_files = FileList["test/isolated/**/*_test.rb"]
1824
end
1925

2026
Rake::TestTask.new(:engine_test) do |t|
@@ -176,6 +182,10 @@ task :all_tests do
176182
Rake::Task["test"].invoke
177183
puts
178184
puts
185+
puts "Running isolated Minitests"
186+
Rake::Task["isolated_test"].invoke
187+
puts
188+
puts
179189
puts "Running Minitests for Rails Engine compatibility"
180190
Rake::Task["engine_test"].invoke
181191
puts

‎docs/CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ nav_order: 6
1010

1111
## main
1212

13+
* Only register the executor hook that watches component templates when Action View doesn't cache templates at boot. With template caching on, as in production, the hook did nothing but still read `ActionView::Base.cache_template_loading` on every request. That value lives in a class variable, which non-main Ractors can't read on Ruby 4.0, so under Rails' experimental `Rails.application.ractorize!` every request failed.
14+
15+
*Mikael Henriksson*
16+
1317
* Fix `Component.translate` raising `UncaughtThrowError` for keys missing from the sidecar translations, and `NoMethodError` for components without sidecar translations. It now falls back to the global translations, like the instance method does. Fix `translate` returning the `default:` value instead of an existing global translation.
1418

1519
*Andrii Furmanets*

‎lib/view_component/engine.rb‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,19 @@ class Engine < Rails::Engine # :nodoc:
132132
end
133133
end
134134

135-
template_watcher = nil
136-
137-
app.executor.to_run :before do
135+
# Like Rails' own template cache expiry, only watch templates when they
136+
# aren't cached at boot. With template caching on, as in production, no
137+
# hook runs per request, so requests never read ActionView's caching
138+
# class variable, which non-main Ractors can't access.
139+
ActiveSupport.on_load(:action_view) do
138140
next if ActionView::Base.cache_template_loading
139141

140-
template_watcher ||= CompileCache.template_watcher(app.config.file_watcher)
141-
template_watcher.execute_if_updated
142+
template_watcher = nil
143+
144+
app.executor.to_run :before do
145+
template_watcher ||= CompileCache.template_watcher(app.config.file_watcher)
146+
template_watcher.execute_if_updated
147+
end
142148
end
143149

144150
app.reloader.before_class_unload { CompileCache.invalidate! }
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# frozen_string_literal: true
2+
3+
require "bundler/setup"
4+
require "minitest/autorun"
5+
require "minitest/mock"
6+
require "tmpdir"
7+
require "rails"
8+
require "action_controller/railtie"
9+
require "active_support/testing/isolation"
10+
require "view_component"
11+
12+
# Boots a fresh application per test in a forked process, so tests can choose
13+
# their own environment. Runs separately from the sandbox, which boots in test.
14+
class TemplateCachingTest < ActiveSupport::TestCase
15+
include ActiveSupport::Testing::Isolation
16+
17+
def test_executor_checks_for_template_changes_when_templates_are_not_cached
18+
app = boot_app("development")
19+
refute ActionView::Base.cache_template_loading
20+
21+
watcher = Minitest::Mock.new
22+
watcher.expect(:execute_if_updated, nil)
23+
ViewComponent::CompileCache.stub(:template_watcher, watcher) { app.executor.wrap {} }
24+
25+
watcher.verify
26+
end
27+
28+
# Non-main Ractors can't read ActionView::Resolver's class variable on Ruby 4.0,
29+
# so the executor must not read it on every request when templates are cached.
30+
def test_executor_does_nothing_when_templates_are_cached
31+
app = boot_app("production")
32+
assert ActionView::Base.cache_template_loading
33+
34+
reads = 0
35+
ActionView::Resolver.singleton_class.prepend(Module.new do
36+
define_method(:caching?) do
37+
reads += 1
38+
super()
39+
end
40+
end)
41+
ViewComponent::CompileCache.stub(:template_watcher, ->(*) { flunk "templates were watched" }) { app.executor.wrap {} }
42+
43+
assert_equal 0, reads
44+
end
45+
46+
def test_template_changes_are_not_reflected_on_new_request_when_templates_are_cached
47+
Dir.mktmpdir do |root|
48+
write_file(root, "app/components/greeting_component.rb", "class GreetingComponent < ViewComponent::Base; end")
49+
write_file(root, "app/components/greeting_component.html.erb", "<div>Hello</div>")
50+
51+
app = boot_app("production", root: root)
52+
app.routes.draw do
53+
get "/", to: ->(_env) { [200, {"content-type" => "text/html"}, [ActionController::Base.render(GreetingComponent.new)]] }
54+
end
55+
request = Rack::MockRequest.new(app)
56+
57+
assert_equal "<div>Hello</div>", request.get("/").body.strip
58+
59+
write_file(root, "app/components/greeting_component.html.erb", "<div>Goodbye</div>")
60+
61+
assert_equal "<div>Hello</div>", request.get("/").body.strip
62+
end
63+
end
64+
65+
private
66+
67+
def boot_app(env, root: __dir__)
68+
Rails.env = env
69+
70+
Class.new(Rails::Application) do
71+
config.root = root
72+
config.eager_load = env == "production"
73+
config.enable_reloading = env == "development"
74+
config.secret_key_base = "test"
75+
config.logger = Logger.new(nil)
76+
end.tap(&:initialize!)
77+
end
78+
79+
def write_file(root, path, content)
80+
FileUtils.mkdir_p(File.dirname(File.join(root, path)))
81+
File.write(File.join(root, path), content)
82+
end
83+
end

‎test/sandbox/test/integration_test.rb‎

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,6 @@ def test_rendering_component_in_a_controller_with_block
5050
assert_response :success
5151
end
5252

53-
def test_template_changes_are_not_reflected_on_new_request_when_cache_template_loading_is_true
54-
with_template_caching do
55-
get "/controller_inline"
56-
assert_select("div", "bar")
57-
assert_response :success
58-
59-
modify_file "app/components/controller_inline_component.html.erb", "<div>Goodbye world!</div>" do
60-
get "/controller_inline"
61-
assert_select("div", "bar")
62-
assert_response :success
63-
end
64-
65-
get "/controller_inline"
66-
assert_select("div", "bar")
67-
assert_response :success
68-
end
69-
end
70-
7153
def test_template_changes_are_reflected_on_new_request_when_cache_template_loading_is_false
7254
with_new_cache do
7355
get "/controller_inline"

‎test/test_helper.rb‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,6 @@ def with_generate_sidecar(enabled, &block)
128128
with_generate_option(:sidecar, enabled, &block)
129129
end
130130

131-
def with_template_caching
132-
old_cache_template_loading = ActionView::Base.cache_template_loading
133-
ActionView::Base.cache_template_loading = true
134-
135-
yield
136-
ensure
137-
ActionView::Base.cache_template_loading = old_cache_template_loading
138-
end
139-
140131
def with_new_cache
141132
old_cache = ViewComponent::CompileCache.cache
142133
ViewComponent::CompileCache.cache = Set.new

0 commit comments

Comments
 (0)