-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix stale shared data in tests - call ::InertiaRails.reset!
before each rspec example
#108
Conversation
This issue is certainly hard to squash... Thanks for creating this @coreyaus ! I think your solution makes sense, but I'd like to chat with @bknoles a bit before we merge because issues very similar to this have cropped up a couple of times and I'm starting to feel like we're chasing our tail a bit. Let me know if this is a blocker for you since I expect we could merge this regardless of whatever solution we come up with. |
Thanks @BrandonShar! That's useful background to understand. This isn't a major blocker for me - I've just tweaked our tests so they're a little less strict (e.g. ensuring all the data we expect to have is definitely included, and ignoring any additional shared props that are unexpectedly leaking through). That's fine for us at the moment and doesn't weaken our tests too much. Sing out if we can help with anything on this one, and have a wonderful end to the week! |
I think at the root here is a potential design problem – or at least something that's IMO unexpected behavior when using this as a seasoned Rails engineer. What I mean is: The things that are declared as "setters" in the What I'm unclear about is why this is even needed. Wouldn't this be a classic use case for In other words, something like this: module InertiaRails
module Controller
extend ActiveSupport::Concern
included do
class_attribute :shared_plain_data, :shared_blocks
self.shared_plain_data = {}
self.shared_blocks = []
end
class_methods do
def inertia_share(shares, &block)
shared_plain_data.merge!(shares) if shares.any?
shared_blocks << block if block.present?
end
end
def shared_data
# This can now use self.class.shared_plain_data and self.class.shared_blocks respectively
end
end
end Is there something I'm overlooking? Shouldn't such an approach work? |
I just ran into this today with user scopes leaking into each other from @BrandonShar are you able to look into what @clemens has highlighted? |
Hello, @coreyaus @BenMorganMY, i want to help with this issue. Can you guys think we can replicate this with one test case in rspec? |
Looking back, we experienced it when we were trying to set an instance variable that caches the organizations a user belongs to: class Dashboard::BaseController < ApplicationController
include InertiaCurrentUser
before_action :load_organizations
private
def load_organizations
organizations = provider_scope(Organization)
organization_id = cookies[:organization_id]
# We load all of the organizations since it will later be used by inertia
# to be written to the document.
@organizations = organizations.to_a
@organization = if organization_id.present?
organizations.find { _1.id == organization_id } || @organizations.first
else
@organizations.first
end
end
end
module InertiaCurrentUser
extend ActiveSupport::Concern
included do
inertia_share current_user: lambda {
# There appears to be a bug with inertia calling this current_user lambda
# in the devise registrations/confirmations. Return early to prevent calls
# to `@organizations` or `current_user` both of which will be `nil`.
return if current_user.nil?
# Error happens here where it calls `organizations` on what will be `nil`
organizations = (@organizations || current_user.organizations).map do
{ id: _1.id, description: _1.description }
end
current_user.as_json(
only: %w[id email role first_name last_name api_key],
include: [
privilege_set: { except: %i[id user_id created_at updated_at] }
]
).merge(organizations:)
}
end
end So, at initial appearance, the |
Just throwing my unqualified question in here: Why is the shared data stored in the global |
Yea, great discussion here. I think we're in need of a refactor away from storing shared data in a variable on the module. @clemens comment (echoed by @buhrmi ):
is spot on, and all our "fixes" are just workarounds for this fundamental design decision. The history behind it is that when we first built the library we ported over what we saw in the Laravel library fairly closely. Ruby != PHP, however, and the fact that classes are cached in a production process leads to all these complications. Storing data at the controller level was also suggested by @ledermann quite awhile ago. Backing up a bit, what we really want is for shared data to be part of a request. Is a controller class variable the right place for that? Or are there any other options? What do you think @BrandonShar ? |
@bknoles thanks for the fast response. In my opnion in the rails way the controller is almost the request, i think a class variable is the right place. I will revive the PR from @ledermann to we test in our apps. Other problem is that if there is a bug is hard to reproduce, the best approach is to make a failing spec, but its hard to accomplish i have try several ways without succeded |
Actually, I think storing per-request data in a As of today, I don't think it will cause problems if you use InertiaRails "correctly", since you can only pass either plain data (which can safely be shared between requests) or a proc that gets executed via class MyController
before_action :share_some_data, only: [:show]
def index
end
def show
end
protected
def share_some_data
self.class.inertia_share({x: 5})
end
end The index action is going to receive inertia_share {x: 5}, only: [:show] I feel like this comment is a good framing... class variables should be set at boot time. We want to set shared data dynamically during a single request, which is just something different. I think the real answer is to change class MyController
before_action :set_some_shared_data, only: [:certain, :actions] # can safely make this conditional!
protected
def set_some_shared_data
inertia_share {x: something_dynamic} # no need to wrap this in a lambda anymore! (ignoring lazy eval)
end
end That would be a breaking change, so we'd do a major version bump. I'm open to other suggestions, but I'm not aware of a way to set instance or per-request data from a class method. |
So I would assume that @PedroAugustoRamalhoDuarte's implementation works fine because it only ever assigns on instance-level. But yeah, I agree with @bknoles that we should use "simple" instance variables instead just to rule out any accidental assignments. |
Thanks for the link @buhrmi I'd definitely prefer to keep the existing API if we can feel confident that the At a glance, I think this line is setting a class level variable and not an instance variable, but the implementation of I think I'm going to actually have to write some code and tests to fully grok it 😅. Thanks everyone for all the good examples and suggestions! |
Yes, that line is setting a class-level variable. But it's empty. Maybe it should be frozen to prevent mutation. The only place where actual data is assigned is here, which references the class-level and makes it instance-level. At least that's how I parse this whole thing in my head. |
Yes, using class_attribute maybe its not the best ideia, there are several created issues with rails with this problem, and its not used for our use case. The use case for class_attribute is more configuration at boot time. I like you suggestion, using before_action all the time should work, i am searching a solution without breaking changes |
Awesome! Last night I created a failing test last night that demonstrates the issue with I'll check these changes against that test and see what it tells us. |
@bknoles can you share this test with me to I add them to my PR too? |
Sorry all! Couldn't carve out time for this in the last couple weeks. I'm going to merge this PR and cut a new patch release. It's an improvement, and it doesn't make sense to hold it up. I'll make a new issue to continue the larger refactoring discussion. |
Released in |
Notes
We're seeing an issue with
shared_data
making our specs non-deterministic, depending on the running order of our tests (i.e. where a particular controller spec passes individually but fails if it's run after another controller spec).The example code below illustrates the problem we're seeing
When we run the
people_controller_spec.rb
by itself it all works fine.However, if we run the test suite and the
people_controller_spec.rb
runs after thegroups_controller_spec.rb
then we get the following failure message, indicating that theshared_data
is not being successfully cleared between specs:Proposed solution in this PR
You might have preferred ways of resolving this issue but for the sake of suggesting a solution I can confirm that the 1-line change in this PR resolves the issue we're seeing by adding
::InertiaRails.reset!
within theconfig.before(:each, inertia: true) do
block (that reset is called in the middleware layer but I'm guessing it's skipped when running in the test environment)Definitely no dramas if you'd prefer to solve this another way though (i.e. no worries if you need to close this PR unmerged in favour of some other approach!)
Please let me know if it'd be helpful for me to try to add a failing spec to the
inertia_rails
test suite and I can have a go at making that happen based on the example above.Cheers for your great work on this excellent library!