Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6c18b0f
[py] Add class to manage remote/grid server
cgoldberg Apr 24, 2025
77eb0a6
[py] Add class to manage remote/grid server
cgoldberg Apr 24, 2025
5d0f51f
[py] User Server class in conftest
cgoldberg Apr 25, 2025
494d90b
[py] Add server tests
cgoldberg Apr 25, 2025
0c3e09b
Merge branch 'SeleniumHQ:trunk' into py-server-standalone-control
cgoldberg Apr 25, 2025
f49c500
[py] Add another test
cgoldberg Apr 25, 2025
96b4d60
[py] Add exception
cgoldberg Apr 25, 2025
ef6a261
[py] Change exception type
cgoldberg Apr 25, 2025
592684d
[py] Check if process is running before terminating
cgoldberg Apr 25, 2025
74a7808
[py] Close socket
cgoldberg Apr 25, 2025
8b4814a
[py] Update docstrings
cgoldberg Apr 25, 2025
3782c84
Merge branch 'SeleniumHQ:trunk' into py-server-standalone-control
cgoldberg Apr 25, 2025
2961951
[py] Fix status_url
cgoldberg Apr 25, 2025
862236f
[py] Move download to nmew method
cgoldberg Apr 25, 2025
27839b4
[py] Add log_level arg
cgoldberg Apr 26, 2025
53ea946
[py] Fix f-string
cgoldberg Apr 26, 2025
0ef0c68
[py] Fix instance check and add unit tests
cgoldberg Apr 26, 2025
5ff2ebb
[py] Update log message
cgoldberg Apr 26, 2025
9408eeb
[py] Download server if none exists from bazel build
cgoldberg Apr 26, 2025
d5ecf61
[py] Fix print
cgoldberg Apr 26, 2025
d6fadbb
[py] Fix linting error
cgoldberg Apr 26, 2025
9b51d5c
Merge branch 'SeleniumHQ:trunk' into py-server-standalone-control
cgoldberg Apr 29, 2025
a08d084
Merge branch 'SeleniumHQ:trunk' into py-server-standalone-control
cgoldberg Apr 29, 2025
c63406c
Merge branch 'SeleniumHQ:trunk' into py-server-standalone-control
cgoldberg Apr 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 13 additions & 55 deletions py/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,11 @@

import os
import platform
import socket
import subprocess
import time
from urllib.request import urlopen

import pytest

from selenium import webdriver
from selenium.webdriver.remote.server import Server
from test.selenium.webdriver.common.network import get_lan_ip
from test.selenium.webdriver.common.webserver import SimpleWebServer

Expand Down Expand Up @@ -125,7 +122,7 @@ def driver(request):

# skip tests in the 'remote' directory if run with a local driver
if request.node.path.parts[-2] == "remote" and driver_class != "Remote":
pytest.skip(f"Remote tests can't be run with driver '{driver_option}'")
pytest.skip(f"Remote tests can't be run with driver '{driver_option.lower()}'")

# skip tests that can't run on certain platforms
_platform = platform.system()
Expand Down Expand Up @@ -295,60 +292,21 @@ def server(request):
yield None
return

_host = "localhost"
_port = 4444
_path = os.path.join(
path = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"java/src/org/openqa/selenium/grid/selenium_server_deploy.jar",
)

def wait_for_server(url, timeout):
start = time.time()
while time.time() - start < timeout:
try:
urlopen(url)
return 1
except OSError:
time.sleep(0.2)
return 0

_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
url = f"http://{_host}:{_port}/status"
try:
_socket.connect((_host, _port))
print(
"The remote driver server is already running or something else"
"is using port {}, continuing...".format(_port)
)
except Exception:
remote_env = os.environ.copy()
if platform.system() == "Linux":
# There are issues with window size/position when running Firefox
# under Wayland, so we use XWayland instead.
remote_env["MOZ_ENABLE_WAYLAND"] = "0"
print("Starting the Selenium server")
process = subprocess.Popen(
[
"java",
"-jar",
_path,
"standalone",
"--port",
"4444",
"--selenium-manager",
"true",
"--enable-managed-downloads",
"true",
],
env=remote_env,
)
print(f"Selenium server running as process: {process.pid}")
assert wait_for_server(url, 10), f"Timed out waiting for Selenium server at {url}"
print("Selenium server is ready")
yield process
process.terminate()
process.wait()
print("Selenium server has been terminated")
remote_env = os.environ.copy()
if platform.system() == "Linux":
# There are issues with window size/position when running Firefox
# under Wayland, so we use XWayland instead.
remote_env["MOZ_ENABLE_WAYLAND"] = "0"

server = Server(path=path, env=remote_env)
server.start()
yield server
server.stop()


@pytest.fixture(autouse=True, scope="session")
Expand Down
1 change: 1 addition & 0 deletions py/docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ Webdriver.remote
selenium.webdriver.remote.mobile
selenium.webdriver.remote.remote_connection
selenium.webdriver.remote.script_key
selenium.webdriver.remote.server
selenium.webdriver.remote.shadowroot
selenium.webdriver.remote.switch_to
selenium.webdriver.remote.utils
Expand Down
154 changes: 154 additions & 0 deletions py/selenium/webdriver/remote/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

import os
import re
import shutil
import socket
import subprocess
import time
import urllib

from selenium.webdriver.common.selenium_manager import SeleniumManager


class Server:
"""Manage a Selenium Grid (Remote) Server in standalone mode.

This class contains functionality for downloading the server and starting/stopping it.

For more information on Selenium Grid, see:
- https://www.selenium.dev/documentation/grid/getting_started/

Parameters:
-----------
host : str
Hostname or IP address to bind to (determined automatically if not specified)
port : int or str
Port to listen on (4444 if not specified)
path : str
Path/filename of existing server .jar file (Selenium Manager is used if not specified)
version : str
Version of server to download (latest version if not specified)
env: dict
Environment variables passed to server environment
"""

def __init__(self, host=None, port=4444, path=None, version=None, env=None):
if path and version:
raise TypeError("Not allowed to specify a version when using an existing server path")

self.host = host
self.port = self._validate_port(port)
self.path = self._validate_path(path)
self.version = self._validate_version(version)
self.env = env

self.process = None
self.status_url = self._get_status_url()

def _get_status_url(self):
host = self.host if self.host is not None else "localhost"
return f"http://{host}:{self.port}/status"

def _validate_path(self, path):
if path and not os.path.exists(path):
raise OSError(f"Can't find server .jar located at {path}")
return path

def _validate_port(self, port):
try:
port = int(port)
except ValueError:
raise TypeError(f"{__class__.__name__}.__init__() got an invalid port: '{port}'")
if not (0 <= port <= 65535):
raise ValueError("port must be 0-65535")
return port

def _validate_version(self, version):
if version:
if not re.match(r"^\d+\.\d+\.\d+$", str(version)):
raise TypeError(f"{__class__.__name__}.__init__() got an invalid version: '{version}'")
return version

def _wait_for_server(self, timeout=10):
start = time.time()
while time.time() - start < timeout:
try:
urllib.request.urlopen(self.status_url)
return True
except urllib.error.URLError:
time.sleep(0.2)
return False

def start(self):
"""Start the server.

Selenium Manager will detect the server location and download it if necessary,
unless an existing server path was specified.
"""
if self.path is None:
selenium_manager = SeleniumManager()
args = ["--grid"]
if self.version:
args.append(self.version)
self.path = selenium_manager.binary_paths(args)["driver_path"]

java_path = shutil.which("java")
if java_path is None:
raise OSError("Can't find java on system PATH. JRE is required to run the Selenium server")

command = [
java_path,
"-jar",
self.path,
"standalone",
"--port",
str(self.port),
"--selenium-manager",
"true",
"--enable-managed-downloads",
"true",
]
if self.host is not None:
command.extend(["--host", self.host])

host = self.host if self.host is not None else "localhost"

try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((host, self.port))
raise ConnectionError(f"Selenium server is already running, or something else is using port {self.port}")
except ConnectionRefusedError:
print(f"Starting Selenium server at: {self.path}")
self.process = subprocess.Popen(command, env=self.env)
print(f"Selenium server running as process: {self.process.pid}")
if not self._wait_for_server():
raise TimeoutError(f"Timed out waiting for Selenium server at {self.status_url}")
print("Selenium server is ready")
return self.process

def stop(self):
"""Stop the server."""
if self.process is None:
raise RuntimeError("Selenium server isn't running")
else:
if self.process.poll() is None:
self.process.terminate()
self.process.wait()
self.process = None
print("Selenium server has been terminated")
56 changes: 56 additions & 0 deletions py/test/unit/selenium/webdriver/remote/remote_server_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

import re

import pytest

from selenium.webdriver.remote.server import Server


def test_server_with_bad_path():
path = "/path/to/nowhere"
msg = f"Can't find server .jar located at {path}"
with pytest.raises(OSError, match=re.escape(msg)):
Server(path=path)


def test_server_with_invalid_version():
versions = ("0.0", "invalid")
for version in versions:
msg = f"Server.__init__() got an invalid version: '{version}'"
with pytest.raises(TypeError, match=re.escape(msg)):
Server(version=version)


def test_server_with_invalid_port():
port = "invalid"
msg = f"Server.__init__() got an invalid port: '{port}'"
with pytest.raises(TypeError, match=re.escape(msg)):
Server(port=port)


def test_server_with_port_out_of_range():
with pytest.raises(ValueError, match="port must be 0-65535"):
Server(port=99999)


def test_stopping_server_thats_not_running():
server = Server()
with pytest.raises(RuntimeError, match="Selenium server isn't running"):
server.stop()
assert server.process is None