diff --git a/dotnet/test/testing.nunit/Environment/EnvironmentManager.cs b/dotnet/test/testing.nunit/Environment/EnvironmentManager.cs index abee6831cfccc..9acb2e26ec20b 100644 --- a/dotnet/test/testing.nunit/Environment/EnvironmentManager.cs +++ b/dotnet/test/testing.nunit/Environment/EnvironmentManager.cs @@ -28,6 +28,15 @@ public class EnvironmentManager { private static readonly Lazy instance = new(() => new EnvironmentManager()); + private static readonly Dictionary driverPathEnvironmentVariables = new() + { + [Browser.Chrome] = "SE_CHROMEDRIVER", + [Browser.Edge] = "SE_EDGEDRIVER", + [Browser.Firefox] = "SE_GECKODRIVER", + [Browser.IE] = "SE_IEDRIVER", + [Browser.Safari] = "SE_SAFARIDRIVER", + }; + private EnvironmentManager() { string dataFilePath; @@ -73,6 +82,13 @@ private EnvironmentManager() Browser = driverConfig.BrowserValue; RemoteCapabilities = driverConfig.RemoteCapabilities; + // Services the tests build themselves then resolve the pinned driver instead of invoking Selenium Manager. + if (!string.IsNullOrEmpty(driverServiceLocation) + && driverPathEnvironmentVariables.TryGetValue(Browser, out string driverVariable)) + { + System.Environment.SetEnvironmentVariable(driverVariable, driverServiceLocation); + } + WebServer = new AppServer(); WebServer.StartAsync().GetAwaiter().GetResult(); diff --git a/dotnet/test/webdriver/Firefox/FirefoxCommandContextTests.cs b/dotnet/test/webdriver/Firefox/FirefoxCommandContextTests.cs index f3b69324b144d..4b53f77f38063 100644 --- a/dotnet/test/webdriver/Firefox/FirefoxCommandContextTests.cs +++ b/dotnet/test/webdriver/Firefox/FirefoxCommandContextTests.cs @@ -18,6 +18,7 @@ // using OpenQA.Selenium.Firefox; +using OpenQA.Selenium.Testing.NUnit.Environment; namespace OpenQA.Selenium.Tests.Firefox; @@ -34,7 +35,10 @@ public void ShouldGetAndSetContext() FirefoxDriverService service = FirefoxDriverService.CreateDefaultService(); service.AllowSystemAccess = true; - FirefoxDriver driver = new FirefoxDriver(service); + FirefoxOptions options = new FirefoxOptions(); + options.BinaryLocation = EnvironmentManager.Instance.BrowserLocation; + + FirefoxDriver driver = new FirefoxDriver(service, options); try { Assert.That(driver.GetContext(), Is.EqualTo(FirefoxCommandContext.Content)); diff --git a/dotnet/test/webdriver/Firefox/FirefoxDriverServiceTests.cs b/dotnet/test/webdriver/Firefox/FirefoxDriverServiceTests.cs index fbc3d20038a38..c673ab15662b6 100644 --- a/dotnet/test/webdriver/Firefox/FirefoxDriverServiceTests.cs +++ b/dotnet/test/webdriver/Firefox/FirefoxDriverServiceTests.cs @@ -20,6 +20,7 @@ using System.IO; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.Internal.Logging; +using OpenQA.Selenium.Testing.NUnit.Environment; namespace OpenQA.Selenium.Tests.Firefox; @@ -56,6 +57,7 @@ public void TearDown() public void ShouldRedirectGeckoDriverLogsToFile() { FirefoxOptions options = new FirefoxOptions(); + options.BinaryLocation = EnvironmentManager.Instance.BrowserLocation; string logPath = Path.GetTempFileName(); options.LogLevel = FirefoxDriverLogLevel.Info; @@ -82,6 +84,7 @@ public void ShouldRedirectGeckoDriverLogsToConsole() { Log.SetLevel(LogEventLevel.Trace).Handlers.Add(testLogHandler); FirefoxOptions options = new FirefoxOptions(); + options.BinaryLocation = EnvironmentManager.Instance.BrowserLocation; options.LogLevel = FirefoxDriverLogLevel.Info; FirefoxDriverService service = FirefoxDriverService.CreateDefaultService(); diff --git a/py/conftest.py b/py/conftest.py index 1c3316274f6e6..9d64d3fa23b7c 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -251,6 +251,24 @@ def _pinned_grid_args(config): ] +# Maps the test driver name to the env var its Service consults before invoking Selenium Manager. +_DRIVER_PATH_ENV_KEYS = { + "chrome": "SE_CHROMEDRIVER", + "edge": "SE_EDGEDRIVER", + "firefox": "SE_GECKODRIVER", + "ie": "SE_IEDRIVER", + "safari": "SE_SAFARIDRIVER", +} + + +def pytest_configure(config): + """Export the pinned driver so services the tests build without a path skip Selenium Manager.""" + executable = config.option.executable + driver = next((d for d in config.option.drivers or [] if d.lower() in _DRIVER_PATH_ENV_KEYS), None) + if driver and executable: + os.environ[_DRIVER_PATH_ENV_KEYS[driver.lower()]] = _resolve_bazel_path(executable).strip("'") + + def get_extensions_location(): """Locate the test extensions directory. diff --git a/py/test/selenium/webdriver/chrome/chrome_service_tests.py b/py/test/selenium/webdriver/chrome/chrome_service_tests.py index 7fd1bd8193605..d24f4b22a7f15 100644 --- a/py/test/selenium/webdriver/chrome/chrome_service_tests.py +++ b/py/test/selenium/webdriver/chrome/chrome_service_tests.py @@ -152,10 +152,8 @@ class TestChromeDriverService: service_path = "/path/to/chromedriver" @pytest.fixture(autouse=True) - def setup_and_teardown(self): - os.environ["SE_CHROMEDRIVER"] = self.service_path - yield - os.environ.pop("SE_CHROMEDRIVER", None) + def pin_driver_env(self, monkeypatch): + monkeypatch.setenv("SE_CHROMEDRIVER", self.service_path) def test_uses_path_from_env_variable(self, service): assert "chromedriver" in service.path diff --git a/py/test/selenium/webdriver/edge/edge_service_tests.py b/py/test/selenium/webdriver/edge/edge_service_tests.py index 8443bacafca4a..336a233fb8a18 100644 --- a/py/test/selenium/webdriver/edge/edge_service_tests.py +++ b/py/test/selenium/webdriver/edge/edge_service_tests.py @@ -152,10 +152,8 @@ class TestEdgeDriverService: service_path = "/path/to/msedgedriver" @pytest.fixture(autouse=True) - def setup_and_teardown(self): - os.environ["SE_EDGEDRIVER"] = self.service_path - yield - os.environ.pop("SE_EDGEDRIVER", None) + def pin_driver_env(self, monkeypatch): + monkeypatch.setenv("SE_EDGEDRIVER", self.service_path) def test_uses_path_from_env_variable(self, service): assert "msedgedriver" in service.path diff --git a/py/test/selenium/webdriver/firefox/firefox_context_tests.py b/py/test/selenium/webdriver/firefox/firefox_context_tests.py index 6e2d075372435..8782b219a6a1a 100644 --- a/py/test/selenium/webdriver/firefox/firefox_context_tests.py +++ b/py/test/selenium/webdriver/firefox/firefox_context_tests.py @@ -18,17 +18,15 @@ import pytest from selenium.webdriver import Firefox -from selenium.webdriver.firefox.options import Options from selenium.webdriver.firefox.service import Service @pytest.fixture -def driver(request): +def driver(request, clean_options): if request.config.getoption("remote"): pytest.skip("system access cannot be granted per-session on Grid") - options = Options() service = Service(service_args=["--allow-system-access"]) - driver = Firefox(options=options, service=service) + driver = Firefox(options=clean_options, service=service) yield driver driver.quit() diff --git a/py/test/selenium/webdriver/firefox/firefox_service_tests.py b/py/test/selenium/webdriver/firefox/firefox_service_tests.py index 26cfad8e93e9a..d9bf502219497 100644 --- a/py/test/selenium/webdriver/firefox/firefox_service_tests.py +++ b/py/test/selenium/webdriver/firefox/firefox_service_tests.py @@ -24,7 +24,6 @@ from selenium.common.exceptions import SessionNotCreatedException from selenium.webdriver import Firefox -from selenium.webdriver.firefox.options import Options from selenium.webdriver.firefox.service import Service @@ -34,11 +33,11 @@ def _skip_on_grid(request): pytest.skip("driver-service behaviour is only exercised with a local driver") -def test_log_output_as_filename(recwarn) -> None: +def test_log_output_as_filename(recwarn, clean_options) -> None: log_file = "geckodriver.log" service = Service(log_output=log_file) try: - driver = Firefox(service=service) + driver = Firefox(service=service, options=clean_options) assert len(recwarn) == 0 with open(log_file) as fp: assert "geckodriver\tINFO\tListening" in fp.readline() @@ -47,12 +46,12 @@ def test_log_output_as_filename(recwarn) -> None: os.remove(log_file) -def test_log_output_as_file() -> None: +def test_log_output_as_file(clean_options) -> None: log_name = "geckodriver.log" log_file = open(log_name, "w", encoding="utf-8") service = Service(log_output=log_file) try: - driver = Firefox(service=service) + driver = Firefox(service=service, options=clean_options) with open(log_name) as fp: assert "geckodriver\tINFO\tListening" in fp.readline() finally: @@ -61,21 +60,20 @@ def test_log_output_as_file() -> None: os.remove(log_name) -def test_log_output_as_stdout(capfd) -> None: +def test_log_output_as_stdout(capfd, clean_options) -> None: service = Service(log_output=subprocess.STDOUT) - driver = Firefox(service=service) + driver = Firefox(service=service, options=clean_options) out, err = capfd.readouterr() assert "geckodriver\tINFO\tListening" in out driver.quit() -def test_driver_is_stopped_if_browser_cant_start(clean_driver) -> None: - options = Options() - options.add_argument("-profile=/no/such/location") +def test_driver_is_stopped_if_browser_cant_start(clean_driver, clean_options) -> None: + clean_options.add_argument("-profile=/no/such/location") service = Service() with pytest.raises(SessionNotCreatedException): - clean_driver(options=options, service=service) + clean_driver(options=clean_options, service=service) assert not service.is_connectable() assert service.process.poll() is not None @@ -109,10 +107,8 @@ class TestGeckoDriverService: service_path = "/path/to/geckodriver" @pytest.fixture(autouse=True) - def setup_and_teardown(self): - os.environ["SE_GECKODRIVER"] = self.service_path - yield - os.environ.pop("SE_GECKODRIVER", None) + def pin_driver_env(self, monkeypatch): + monkeypatch.setenv("SE_GECKODRIVER", self.service_path) def test_uses_path_from_env_variable(self, service): assert "geckodriver" in service.path diff --git a/py/test/selenium/webdriver/safari/safari_service_tests.py b/py/test/selenium/webdriver/safari/safari_service_tests.py index 655fdcfe78995..4c5149750cbb6 100644 --- a/py/test/selenium/webdriver/safari/safari_service_tests.py +++ b/py/test/selenium/webdriver/safari/safari_service_tests.py @@ -15,7 +15,6 @@ # specific language governing permissions and limitations # under the License. -import os import sys from unittest.mock import patch @@ -34,10 +33,8 @@ class TestSafariDriverService: service_path = "/path/to/safaridriver" @pytest.fixture(autouse=True) - def setup_and_teardown(self): - os.environ["SE_SAFARIDRIVER"] = self.service_path - yield - os.environ.pop("SE_SAFARIDRIVER", None) + def pin_driver_env(self, monkeypatch): + monkeypatch.setenv("SE_SAFARIDRIVER", self.service_path) def test_uses_path_from_env_variable(self, service): assert "safaridriver" in service.path diff --git a/rb/spec/integration/selenium/webdriver/chrome/service_spec.rb b/rb/spec/integration/selenium/webdriver/chrome/service_spec.rb index d86a0d7afdfec..e29e53946b2e1 100644 --- a/rb/spec/integration/selenium/webdriver/chrome/service_spec.rb +++ b/rb/spec/integration/selenium/webdriver/chrome/service_spec.rb @@ -30,12 +30,6 @@ module Chrome after { service_manager.stop } - it 'auto uses chromedriver' do - service.executable_path = DriverFinder.new(nil, described_class.new).driver_path - - expect(service_manager.uri).to be_a(URI) - end - it 'can be started outside driver' do expect(service_manager.uri).to be_a(URI) end diff --git a/rb/spec/integration/selenium/webdriver/driver_finder_spec.rb b/rb/spec/integration/selenium/webdriver/driver_finder_spec.rb index c1e8185d0cfd0..1054185369885 100644 --- a/rb/spec/integration/selenium/webdriver/driver_finder_spec.rb +++ b/rb/spec/integration/selenium/webdriver/driver_finder_spec.rb @@ -33,12 +33,14 @@ module WebDriver expect { Platform.assert_executable(driver_finder.driver_path) }.not_to raise_error end - it 'resolves an executable browser path' do + it 'resolves an executable browser path', + skip_if: {pinned: true, reason: 'pinned runs supply the browser through options'} do expect { Platform.assert_executable(driver_finder.browser_path) }.not_to raise_error end it 'downloads the driver into the Selenium cache', - pending_if: {browser: :safari, reason: 'driver ships with OS'} do + pending_if: {browser: :safari, reason: 'driver ships with OS'}, + skip_if: {pinned: true, reason: 'pinned runs do not use Selenium Manager'} do Dir.mktmpdir('se-cache') do |cache_dir| originals = {'SE_CACHE_PATH' => ENV.fetch('SE_CACHE_PATH', nil), 'SE_SKIP_DRIVER_IN_PATH' => ENV.fetch('SE_SKIP_DRIVER_IN_PATH', nil)} @@ -54,7 +56,8 @@ module WebDriver it 'downloads the browser into the Selenium cache', pending_if: [{browser: :safari, reason: 'browser ships with OS'}, {browser: :edge, platform: :windows, - reason: 'Edge MSI installer always writes to system path'}] do + reason: 'Edge MSI installer always writes to system path'}], + skip_if: {pinned: true, reason: 'pinned runs do not use Selenium Manager'} do Dir.mktmpdir('se-cache') do |cache_dir| originals = {'SE_CACHE_PATH' => ENV.fetch('SE_CACHE_PATH', nil), 'SE_FORCE_BROWSER_DOWNLOAD' => ENV.fetch('SE_FORCE_BROWSER_DOWNLOAD', nil)} @@ -68,6 +71,7 @@ module WebDriver end it 'resolves the browser to its system install location', + skip_if: {pinned: true, reason: 'pinned runs do not use Selenium Manager'}, skip_unless: [{browser: :safari}, {browser: :edge, platform: :windows}] do Dir.mktmpdir('se-cache') do |cache_dir| diff --git a/rb/spec/integration/selenium/webdriver/edge/service_spec.rb b/rb/spec/integration/selenium/webdriver/edge/service_spec.rb index 05fd5e14fd83f..bcd0245433cd0 100644 --- a/rb/spec/integration/selenium/webdriver/edge/service_spec.rb +++ b/rb/spec/integration/selenium/webdriver/edge/service_spec.rb @@ -30,12 +30,6 @@ module Edge after { service_manager.stop } - it 'auto uses edgedriver' do - service.executable_path = DriverFinder.new(nil, described_class.new).driver_path - - expect(service_manager.uri).to be_a(URI) - end - it 'can be started outside driver' do expect(service_manager.uri).to be_a(URI) end diff --git a/rb/spec/integration/selenium/webdriver/firefox/service_spec.rb b/rb/spec/integration/selenium/webdriver/firefox/service_spec.rb index 7fdae46d45824..96581571dc2e2 100644 --- a/rb/spec/integration/selenium/webdriver/firefox/service_spec.rb +++ b/rb/spec/integration/selenium/webdriver/firefox/service_spec.rb @@ -30,12 +30,6 @@ module Firefox after { service_manager.stop } - it 'auto uses geckodriver' do - service.executable_path = DriverFinder.new(nil, described_class.new).driver_path - - expect(service_manager.uri).to be_a(URI) - end - it 'can be started outside driver' do expect(service_manager.uri).to be_a(URI) end diff --git a/rb/spec/integration/selenium/webdriver/ie/service_spec.rb b/rb/spec/integration/selenium/webdriver/ie/service_spec.rb index 2d9b950e77c60..1701ff3e558aa 100644 --- a/rb/spec/integration/selenium/webdriver/ie/service_spec.rb +++ b/rb/spec/integration/selenium/webdriver/ie/service_spec.rb @@ -30,12 +30,6 @@ module IE after { service_manager.stop } - it 'auto uses iedriver' do - service.executable_path = DriverFinder.new(nil, described_class.new).driver_path - - expect(service_manager.uri).to be_a(URI) - end - it 'can be started outside driver' do expect(service_manager.uri).to be_a(URI) end diff --git a/rb/spec/integration/selenium/webdriver/safari/service_spec.rb b/rb/spec/integration/selenium/webdriver/safari/service_spec.rb index d59f783183d76..50c1c8cdb9adb 100644 --- a/rb/spec/integration/selenium/webdriver/safari/service_spec.rb +++ b/rb/spec/integration/selenium/webdriver/safari/service_spec.rb @@ -30,12 +30,6 @@ module Safari after { service_manager.stop } - it 'auto uses safaridriver' do - service.executable_path = DriverFinder.new(nil, described_class.new).driver_path - - expect(service_manager.uri).to be_a(URI) - end - it 'can be started outside driver' do expect(service_manager.uri).to be_a(URI) end diff --git a/rb/spec/integration/selenium/webdriver/spec_helper.rb b/rb/spec/integration/selenium/webdriver/spec_helper.rb index a8e15a3b6b5dd..c7e290e4ec1d6 100644 --- a/rb/spec/integration/selenium/webdriver/spec_helper.rb +++ b/rb/spec/integration/selenium/webdriver/spec_helper.rb @@ -57,6 +57,7 @@ def create_guards(example) guards.add_condition(:headless, !ENV['HEADLESS'].nil?) guards.add_condition(:bidi, !ENV['WEBDRIVER_BIDI'].nil?) guards.add_condition(:rbe, GlobalTestEnv.rbe?) + guards.add_condition(:pinned, !GlobalTestEnv.driver_path.nil?) guards.add_condition(:version, GlobalTestEnv.browser_version) guards end diff --git a/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb b/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb index a055915a888bd..09d7430022edb 100644 --- a/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb +++ b/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb @@ -41,6 +41,7 @@ def initialize @driver = ENV.fetch('WD_SPEC_DRIVER', 'chrome').tr('-', '_').to_sym @driver_instance = nil @remote_server = nil + pin_driver_path end def print_env @@ -252,6 +253,14 @@ def create_driver!(listener: nil, http_client: nil, service: nil, **, &block) private + # The class-level path also reaches the services that specs build themselves. + def pin_driver_path + path = driver_path + return unless path + + WebDriver::Service.send(browser).class.driver_path = path + end + def new_driver_instance(**) method = :"#{driver}_driver" instance = private_methods.include?(method) ? send(method, **) : WebDriver::Driver.for(driver, **) @@ -276,12 +285,13 @@ def print_driver_version(instance) WebDriver.logger.warn("could not read driver version from #{path}: #{e.message}") end - def build_options(**) + def build_options(**opts) + opts[:binary] ||= browser_path if browser_path options_method = :"#{browser}_options" if private_methods.include?(options_method) - send(options_method, **) + send(options_method, **opts) else - WebDriver::Options.send(browser, **) + WebDriver::Options.send(browser, **opts) end end @@ -345,7 +355,6 @@ def chrome_driver(service: nil, **) service ||= WebDriver::Service.chrome service.args << '--disable-build-check' if ENV['DISABLE_BUILD_CHECK'] service.args << '--verbose' if WebDriver.logger.debug? - service.executable_path = runfiles_path('CHROMEDRIVER_BINARY') if ENV.key?('CHROMEDRIVER_BINARY') WebDriver::Driver.for(:chrome, service: service, **) end @@ -353,14 +362,12 @@ def edge_driver(service: nil, **) service ||= WebDriver::Service.edge service.args << '--disable-build-check' if ENV['DISABLE_BUILD_CHECK'] service.args << '--verbose' if WebDriver.logger.debug? - service.executable_path = runfiles_path('MSEDGEDRIVER_BINARY') if ENV.key?('MSEDGEDRIVER_BINARY') WebDriver::Driver.for(:edge, service: service, **) end def firefox_driver(service: nil, **) service ||= WebDriver::Service.firefox service.args.push('--log', 'trace') if WebDriver.logger.debug? - service.executable_path = runfiles_path('GECKODRIVER_BINARY') if ENV.key?('GECKODRIVER_BINARY') WebDriver::Driver.for(:firefox, service: service, **) end @@ -379,7 +386,6 @@ def safari_preview_driver(**) def chrome_options(args: [], **opts) opts[:browser_version] = browser_version opts[:web_socket_url] = true if ENV['WEBDRIVER_BIDI'] && !opts.key?(:web_socket_url) - opts[:binary] ||= runfiles_path('CHROME_BINARY') if ENV.key?('CHROME_BINARY') args << '--headless' if ENV['HEADLESS'] args << '--no-sandbox' unless Platform.windows? args << '--disable-dev-shm-usage' if GlobalTestEnv.rbe? @@ -390,7 +396,6 @@ def chrome_options(args: [], **opts) def edge_options(args: [], **opts) opts[:browser_version] = browser_version opts[:web_socket_url] = true if ENV['WEBDRIVER_BIDI'] && !opts.key?(:web_socket_url) - opts[:binary] ||= runfiles_path('EDGE_BINARY') if ENV.key?('EDGE_BINARY') args << '--headless' if ENV['HEADLESS'] args << '--no-sandbox' unless Platform.windows? args << '--disable-dev-shm-usage' if GlobalTestEnv.rbe? @@ -401,7 +406,6 @@ def edge_options(args: [], **opts) def firefox_options(args: [], **opts) opts[:browser_version] = browser_version opts[:web_socket_url] = true if ENV['WEBDRIVER_BIDI'] && !opts.key?(:web_socket_url) - opts[:binary] ||= runfiles_path('FIREFOX_BINARY') if ENV.key?('FIREFOX_BINARY') opts[:unhandled_prompt_behavior] ||= 'ignore' args << '--headless' if ENV['HEADLESS'] WebDriver::Options.firefox(args: args, **opts)