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
Preload Selenium driver_path before parallelizing system tests #49908
Preload Selenium driver_path before parallelizing system tests #49908
Conversation
f60821b
to
1c1da2c
Compare
@@ -148,7 +148,7 @@ class DriverTest < ActiveSupport::TestCase | |||
end | |||
end | |||
|
|||
test "preloads browser's driver_path" do | |||
test "preloads browser's driver_path if it is set to a Proc" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we drop service.driver_path.try(:call)
, I think we can drop this test too, because Rails would no longer be responsible for this behavior.
Thought it might be worth adding a test for when driver_path
is set to an actual path (to verify that it is not overwritten).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I'll drop the Proc test and add another test to verify an existing value is not overwritten.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 916efae770 and 868599937a
found_executable = RbConfig.ruby | ||
::Selenium::WebDriver::DriverFinder.stub(:path, found_executable) do | ||
ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :chrome) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm hesitant to stub DriverFinder.path
since we're relying on it directly. Does this test blow up if we don't stub? Perhaps it would be slightly better to stub SeleniumManager.driver_path
, which DriverFinder.path
calls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used a stub to avoid a network call to download chromedriver, which would slow down the test and perhaps make it less reliable. However I agree that the test is more realistic and valuable without the stub. I'll remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in be39f0c695
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to avoid a network call to download chromedriver
Ah, right, in that case we should stub. (Sorry! 🙏) But I think stubbing SeleniumManager.driver_path
might be a better choice because if the behavior of DriverFinder.path
changes, then we will be more likely to catch it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, addressed in 3f089b3aa1 ✅
8685999
to
deaf466
Compare
@jonathanhefner I've addressed your review comments and squashed down to 1 commit. |
3f089b3
to
378991a
Compare
@jonathanhefner thanks for your feedback and your patience. This is ready for another review. |
When the webdrivers gem is not present (which is the default scenario in Rails 7.1+), the Selenium `driver_path` starts out as `nil`. This means the driver is located lazily, and deferred until a system test is run. If parallel testing is used, this leads to a race condition, where each worker process tries to resolve the driver simultaneously. The result is an error as described in rails#49906. This commit fixes the race condition by changing the implementation of `Browser#preload`. The previous implementation worked when `driver_path` was set to a Proc by the `webdrivers` gem, but doesn't work when the `webdrivers` gem is not being used and the `driver_path` is `nil`. `Browser#preload` now uses the `DriverFinder` utility provided by the `selenium-webdriver` gem to eagerly resolve the driver path if needed. This will ensures that `driver_path` is set before parallel test workers are forked. Fixes rails#49906. Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
378991a
to
e7d743b
Compare
Looks good! Though I pushed a commit to change Thank you, @mattbrictson! 🏎️ |
Preload Selenium driver_path before parallelizing system tests
I think this might break system tests where the Chrome binary is not in the default path, e.g. where |
OK, here's what I think is happening. Deep in With this change, we are setting Is this a bug in Rails? Or an issue with |
@lazyatom Thank you for investigating, but I'm not sure I follow. Why does |
@jonathanhefner thanks for replying. I've created a new issue (#50287) with a replication script and a hopefully-clearer description/explanation. The only thing I can't achieve with the script is making sure that you don't already have Chrome installed -- that's key to demonstrating this issue. |
Follow-up to rails#49908. When Selenium resolves the driver path to a copy of Chrome that is has downloaded / cached, it mutates the `Selenium::WebDriver::Chrome::Options` object it receives, and relies on those changes later when the options are used. If `Selenium::WebDriver::Chrome::Service.driver_path` is set but a different options object is used, Selenium will raise "cannot find Chrome binary". Therefore, this commit ensures that the options object passed to `Selenium::WebDriver::DriverFinder.path` is the same options object used by the driver later.
Follow-up to rails#49908. When Selenium resolves the driver path to a copy of Chrome that it has downloaded / cached, it mutates the `Selenium::WebDriver::Chrome::Options` object it receives, and relies on those changes later when the options are used. If `Selenium::WebDriver::Chrome::Service.driver_path` is set but a different options object is used, Selenium will raise "cannot find Chrome binary". Therefore, this commit ensures that the options object passed to `Selenium::WebDriver::DriverFinder.path` is the same options object used by the driver later.
Hi @mattbrictson, it looks like the Code example that's failing:
With this |
@majkelcc that sounds like it should be a new bug report. I am not verify familiar with how remote browsers work with Selenium, but apparently Rails does not consult rails/actionpack/lib/action_dispatch/system_testing/driver.rb Lines 18 to 19 in 1eda300
I suspect Rails 7.1.1 was working for you because prior to 7.1.2, the |
Motivation / Background
When the
webdrivers
gem is not present (which is the default scenario in Rails 7.1+), the Seleniumdriver_path
starts out asnil
. This means the driver is located lazily, and deferred until a system test is run.If parallel testing is used, this leads to a race condition, where each worker process tries to resolve the driver simultaneously. The result is an error as described in #49906.
Detail
This commit fixes the race condition by changing the implementation of
Browser#preload
. The previous implementation worked whendriver_path
was set to a Proc by thewebdrivers
gem, but doesn't work when thewebdrivers
gem is not being used and thedriver_path
isnil
.If the
driver_path
isnil
,Browser#preload
will now use theDriverFinder
utility provided by theselenium-webdriver
gem to eagerly resolve the driver path. This ensures thatdriver_path
is set before parallel test workers are forked.Fixes #49906.
Additional information
This solution mimics the code within Selenium that determines the driver executable on demand:
https://github.com/SeleniumHQ/selenium/blob/7680b7cf25579217cb62a0893c4b2b69c0186062/rb/lib/selenium/webdriver/common/local_driver.rb#L41
Checklist
Before submitting the PR make sure the following are checked:
[Fix #issue-number]