Skip to content

Commit 3b24d3e

Browse files
committed
Support cache_classes: true descendants filtering
When cache_classes is true, we can no longer rely on the Active Support dependencies tracker, so we use our own separate tracker to filter out destroyed with_model models from Class#descendants and Class#subclasses. Fixes #35
1 parent dd38e8d commit 3b24d3e

5 files changed

Lines changed: 102 additions & 37 deletions

File tree

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,8 @@ RSpec/Be:
2929
RSpec/BeforeAfterAll:
3030
Enabled: false
3131

32+
RSpec/ExpectInHook:
33+
Enabled: false
34+
3235
Style/Documentation:
3336
Enabled: false
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require "active_support/descendants_tracker"
2+
3+
module WithModel
4+
module DescendantsTracker
5+
@excluded_descendants = ActiveSupport::DescendantsTracker::WeakSet.new
6+
7+
class << self
8+
def clear(classes) # :nodoc:
9+
classes.each do |klass|
10+
@excluded_descendants << klass
11+
klass.descendants.each do |descendant|
12+
@excluded_descendants << descendant
13+
end
14+
end
15+
end
16+
17+
def reject!(classes) # :nodoc:
18+
if @excluded_descendants
19+
classes.reject! { |d| @excluded_descendants.include?(d) }
20+
end
21+
classes
22+
end
23+
end
24+
25+
module ReloadedClassesFiltering # :nodoc:
26+
def subclasses
27+
WithModel::DescendantsTracker.reject!(super)
28+
end
29+
30+
def descendants
31+
WithModel::DescendantsTracker.reject!(super)
32+
end
33+
end
34+
end
35+
end
36+
37+
class Class
38+
prepend WithModel::DescendantsTracker::ReloadedClassesFiltering
39+
end
40+
41+
module ActiveSupport
42+
module DescendantsTracker
43+
class << self
44+
attr_reader :clear_disabled
45+
end
46+
end
47+
end

lib/with_model/model.rb

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require "active_support/core_ext/string/inflections"
55
require "English"
66
require "with_model/constant_stubber"
7+
require "with_model/descendants_tracker"
78
require "with_model/methods"
89
require "with_model/table"
910

@@ -38,6 +39,7 @@ def destroy
3839
cleanup_descendants_tracking
3940
reset_dependencies_cache
4041
table.destroy
42+
WithModel::DescendantsTracker.clear([@model])
4143
@model = nil
4244
end
4345

@@ -54,15 +56,8 @@ def setup_model
5456
end
5557

5658
def cleanup_descendants_tracking
57-
if defined?(ActiveSupport::DescendantsTracker)
58-
if ActiveSupport::VERSION::MAJOR >= 7
59-
ActiveSupport::DescendantsTracker.clear([@model])
60-
else
61-
ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).delete(ActiveRecord::Base)
62-
end
63-
elsif @model.superclass.respond_to?(:direct_descendants)
64-
@model.superclass.direct_descendants.delete(@model)
65-
end
59+
ActiveSupport::DescendantsTracker.clear([@model]) \
60+
unless ActiveSupport::DescendantsTracker.clear_disabled
6661
end
6762

6863
def reset_dependencies_cache

spec/descendants_tracking_spec.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
describe "Descendants tracking" do # rubocop:disable RSpec/DescribeClass
6+
with_model :BlogPost do
7+
model do
8+
def self.inspect
9+
"BlogPost class #{object_id}"
10+
end
11+
end
12+
end
13+
14+
def blog_post_classes
15+
ActiveRecord::Base.descendants.select do |c|
16+
c.table_name == BlogPost.table_name
17+
end
18+
end
19+
20+
shared_examples "clearing descendants between test runs" do
21+
it "includes the correct model class in descendants on the first test run" do
22+
expect(blog_post_classes).to eq [BlogPost]
23+
end
24+
25+
it "includes the correct model class in descendants on the second test run" do
26+
expect(blog_post_classes).to eq [BlogPost]
27+
end
28+
end
29+
30+
context "with ActiveSupport::DescendantsTracker (cache_classes: true)" do
31+
before do
32+
expect(ActiveSupport::DescendantsTracker.clear_disabled).to be_falsey
33+
expect { ActiveSupport::DescendantsTracker.clear([]) }.not_to raise_exception
34+
end
35+
36+
include_examples "clearing descendants between test runs"
37+
end
38+
39+
context "without ActiveSupport::DescendantsTracker (cache_classes: false)" do
40+
before do
41+
ActiveSupport::DescendantsTracker.disable_clear!
42+
expect(ActiveSupport::DescendantsTracker.clear_disabled).to be_truthy
43+
expect { ActiveSupport::DescendantsTracker.clear([]) }.to raise_exception(RuntimeError)
44+
end
45+
46+
include_examples "clearing descendants between test runs"
47+
end
48+
end

spec/with_model_spec.rb

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -320,30 +320,6 @@ def my_method
320320
end
321321
end
322322

323-
context "with ActiveSupport::DescendantsTracker" do
324-
with_model :BlogPost do
325-
model do
326-
def self.inspect
327-
"BlogPost class #{object_id}"
328-
end
329-
end
330-
end
331-
332-
def blog_post_classes
333-
ActiveRecord::Base.descendants.select do |c|
334-
c.table_name == BlogPost.table_name
335-
end
336-
end
337-
338-
it "includes the correct model class in descendants on the first test run" do
339-
expect(blog_post_classes).to eq [BlogPost]
340-
end
341-
342-
it "includes the correct model class in descendants on the second test run" do
343-
expect(blog_post_classes).to eq [BlogPost]
344-
end
345-
end
346-
347323
context "with_model can be run within RSpec :all hook" do
348324
with_model :BlogPost, scope: :all do
349325
table do |t|
@@ -396,10 +372,6 @@ class ApplicationRecordInDifferentDatabase < ActiveRecord::Base # standard:disab
396372
establish_connection(ActiveRecord::Base.connection_pool.db_config.configuration_hash)
397373
end
398374

399-
after(:all) do
400-
Object.__send__(:remove_const, "ApplicationRecordInDifferentDatabase")
401-
end
402-
403375
with_model :BlogPost, superclass: ApplicationRecordInDifferentDatabase do
404376
table do |t|
405377
t.string "title"

0 commit comments

Comments
 (0)