Skip to content
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

[py] improve driver logging #12103

Merged
merged 3 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion py/selenium/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@
WaitExcTypes = typing.Iterable[typing.Type[Exception]]

# Service Types
SubprocessStdAlias = typing.Union[int, typing.IO[typing.Any]]
SubprocessStdAlias = typing.Union[int, str, typing.IO[typing.Any]]
3 changes: 3 additions & 0 deletions py/selenium/webdriver/chrome/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# under the License.
import typing

from selenium.types import SubprocessStdAlias
from selenium.webdriver.chromium import service

DEFAULT_EXECUTABLE_PATH = "chromedriver"
Expand All @@ -38,6 +39,7 @@ def __init__(
port: int = 0,
service_args: typing.Optional[typing.List[str]] = None,
log_path: typing.Optional[str] = None,
log_output: SubprocessStdAlias = None,
env: typing.Optional[typing.Mapping[str, str]] = None,
**kwargs,
) -> None:
Expand All @@ -46,6 +48,7 @@ def __init__(
port=port,
service_args=service_args,
log_path=log_path,
log_output=log_output,
env=env,
start_error_message="Please see https://chromedriver.chromium.org/home",
**kwargs,
Expand Down
19 changes: 17 additions & 2 deletions py/selenium/webdriver/chromium/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
# specific language governing permissions and limitations
# under the License.
import typing
import warnings

from selenium.common import InvalidArgumentException
from selenium.types import SubprocessStdAlias
from selenium.webdriver.common import service


Expand All @@ -38,18 +41,30 @@ def __init__(
port: int = 0,
service_args: typing.Optional[typing.List[str]] = None,
log_path: typing.Optional[str] = None,
log_output: SubprocessStdAlias = None,
env: typing.Optional[typing.Mapping[str, str]] = None,
start_error_message: typing.Optional[str] = None,
**kwargs,
) -> None:
self.service_args = service_args or []
if log_path:
self.service_args.append(f"--log-path={log_path}")
self.log_output = log_output
if log_path is not None:
warnings.warn("log_path has been deprecated, please use log_output", DeprecationWarning, stacklevel=2)
self.log_output = log_path

if "--append-log" in self.service_args or "--readable-timestamp" in self.service_args:
if isinstance(self.log_output, str):
self.service_args.append(f"--log-path={self.log_output}")
self.log_output = None
else:
msg = "Appending logs and readable timestamps require log output to be a string representing file path"
raise InvalidArgumentException(msg)

super().__init__(
executable=executable_path,
port=port,
env=env,
log_output=self.log_output,
start_error_message=start_error_message,
**kwargs,
)
Expand Down
26 changes: 20 additions & 6 deletions py/selenium/webdriver/common/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import os
import subprocess
import typing
import warnings
from abc import ABC
from abc import abstractmethod
from platform import system
Expand Down Expand Up @@ -51,14 +52,27 @@ def __init__(
self,
executable: str,
port: int = 0,
log_file: SubprocessStdAlias = DEVNULL,
log_file: SubprocessStdAlias = None,
log_output: SubprocessStdAlias = None,
env: typing.Optional[typing.Mapping[typing.Any, typing.Any]] = None,
start_error_message: typing.Optional[str] = None,
**kwargs,
) -> None:
if isinstance(log_output, str):
self.log_output = open(log_output, "a+", encoding="utf-8")
elif log_output is subprocess.STDOUT:
self.log_output = None
elif log_output is None or log_output is subprocess.DEVNULL:
self.log_output = open(os.devnull, "wb")
Comment on lines +61 to +66

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these file descriptors are never closed leading to ResourceWarnings when this is garbage collected (and leaked fds)

else:
self.log_output = log_output

if log_file is not None:
warnings.warn("log_file has been deprecated, please use log_output", DeprecationWarning, stacklevel=2)
self.log_output = open(log_file, "a+", encoding="utf-8")

self._path = executable
self.port = port or utils.free_port()
self.log_file = open(os.devnull, "wb") if not log_file == DEVNULL else log_file
self.start_error_message = start_error_message or ""
# Default value for every python subprocess: subprocess.Popen(..., creationflags=0)
self.popen_kw = kwargs.pop("popen_kw", {})
Expand Down Expand Up @@ -129,10 +143,10 @@ def send_remote_shutdown_command(self) -> None:

def stop(self) -> None:
"""Stops the service."""
if self.log_file != PIPE and not (self.log_file == DEVNULL):
if self.log_output != PIPE and not (self.log_output == DEVNULL):
try:
# Todo: Be explicit in what we are catching here.
if hasattr(self.log_file, "close"):
if hasattr(self.log_output, "close"):
self.log_file.close() # type: ignore
except Exception:
pass
Expand Down Expand Up @@ -195,8 +209,8 @@ def _start_process(self, path: str) -> None:
cmd,
env=self.env,
close_fds=close_file_descriptors,
stdout=self.log_file,
stderr=self.log_file,
stdout=self.log_output,
stderr=self.log_output,
stdin=PIPE,
creationflags=self.creation_flags,
**self.popen_kw,
Expand Down
3 changes: 3 additions & 0 deletions py/selenium/webdriver/edge/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import typing
import warnings

from selenium.types import SubprocessStdAlias
from selenium.webdriver.chromium import service

DEFAULT_EXECUTABLE_PATH = "msedgedriver"
Expand All @@ -41,6 +42,7 @@ def __init__(
port: int = 0,
verbose: bool = False,
log_path: typing.Optional[str] = None,
log_output: SubprocessStdAlias = None,
service_args: typing.Optional[typing.List[str]] = None,
env: typing.Optional[typing.Mapping[str, str]] = None,
**kwargs,
Expand All @@ -60,6 +62,7 @@ def __init__(
port=port,
service_args=service_args,
log_path=log_path,
log_output=log_output,
env=env,
start_error_message="Please download from https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/",
**kwargs,
Expand Down
20 changes: 16 additions & 4 deletions py/selenium/webdriver/firefox/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
# specific language governing permissions and limitations
# under the License.
import typing
import warnings
from typing import List

from selenium.types import SubprocessStdAlias
from selenium.webdriver.common import service
from selenium.webdriver.common import utils

Expand All @@ -41,17 +43,27 @@ def __init__(
port: int = 0,
service_args: typing.Optional[typing.List[str]] = None,
log_path: typing.Optional[str] = None,
log_output: SubprocessStdAlias = None,
env: typing.Optional[typing.Mapping[str, str]] = None,
**kwargs,
) -> None:
# Todo: This is vastly inconsistent, requires a follow up to standardise.
file = log_path or "geckodriver.log"
log_file = open(file, "a+", encoding="utf-8")
self.service_args = service_args or []
if log_path is not None:
warnings.warn("log_path has been deprecated, please use log_output", DeprecationWarning, stacklevel=2)
log_output = open(log_path, "a+", encoding="utf-8")

if log_path is None and log_output is None:
warnings.warn(
"Firefox will soon stop logging to geckodriver.log by default; Specify desired logs with log_output",
DeprecationWarning,
stacklevel=2,
)
log_output = open("geckodriver.log", "a+", encoding="utf-8")

super().__init__(
executable=executable_path,
port=port,
log_file=log_file,
log_output=log_output,
env=env,
**kwargs,
)
Expand Down
5 changes: 5 additions & 0 deletions py/selenium/webdriver/ie/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
# specific language governing permissions and limitations
# under the License.
import typing
import warnings
from typing import List

from selenium.types import SubprocessStdAlias
from selenium.webdriver.common import service

DEFAULT_EXECUTABLE_PATH = "IEDriverServer.exe"
Expand All @@ -31,6 +33,7 @@ def __init__(
port: int = 0,
host: typing.Optional[str] = None,
log_level: typing.Optional[str] = None,
log_output: SubprocessStdAlias = None,
log_file: typing.Optional[str] = None,
**kwargs,
) -> None:
Expand All @@ -51,11 +54,13 @@ def __init__(
if log_level:
self.service_args.append(f"--log-level={log_level}")
if log_file:
warnings.warn("log_file has been deprecated, please use log_output", DeprecationWarning, stacklevel=2)
self.service_args.append(f"--log-file={log_file}")

super().__init__(
executable_path,
port=port,
log_output=log_output,
start_error_message="Please download from https://www.selenium.dev/downloads/ and read up at https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver",
**kwargs,
)
Expand Down
12 changes: 6 additions & 6 deletions py/selenium/webdriver/safari/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
# under the License.

import os
import subprocess
import typing
import warnings

from selenium.webdriver.common import service

Expand All @@ -30,7 +30,7 @@ class Service(service.Service):
:param executable_path: install path of the safaridriver executable, defaults to `/usr/bin/safaridriver`.
:param port: Port for the service to run on, defaults to 0 where the operating system will decide.
:param quiet: Suppress driver stdout & stderr, redirects to os.devnull if enabled.
:param quiet: (Deprecated) Suppress driver stdout & stderr, redirects to os.devnull if enabled.
:param service_args: (Optional) List of args to be passed to the subprocess when launching the executable.
:param env: (Optional) Mapping of environment variables for the new process, defaults to `os.environ`.
"""
Expand All @@ -39,21 +39,21 @@ def __init__(
self,
executable_path: str = DEFAULT_EXECUTABLE_PATH,
port: int = 0,
quiet: bool = False,
quiet: bool = None,
service_args: typing.Optional[typing.List[str]] = None,
env: typing.Optional[typing.Mapping[str, str]] = None,
reuse_service=False,
**kwargs,
) -> None:
self._check_executable(executable_path)
self.service_args = service_args or []
self.quiet = quiet
if quiet is not None:
warnings.warn("quiet is no longer needed to supress output", DeprecationWarning, stacklevel=2)

self._reuse_service = reuse_service
log_file = subprocess.PIPE if not self.quiet else open(os.devnull, "w", encoding="utf-8")
super().__init__(
executable=executable_path,
port=port,
log_file=log_file, # type: ignore
env=env,
**kwargs,
)
Expand Down
94 changes: 94 additions & 0 deletions py/test/selenium/webdriver/chrome/chrome_service_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# 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 subprocess
import time

import pytest

from selenium.webdriver import Chrome
from selenium.webdriver.chrome.service import Service


def test_log_path_deprecated() -> None:
log_path = "chromedriver.log"
msg = "log_path has been deprecated, please use log_output"

with pytest.warns(match=msg, expected_warning=DeprecationWarning):
Service(log_path=log_path)


def test_uses_chromedriver_logging() -> None:
log_file = "chromedriver.log"
service_args = ["--append-log"]

service = Service(log_output=log_file, service_args=service_args)
try:
driver1 = Chrome(service=service)
with open(log_file, "r") as fp:
lines = len(fp.readlines())
driver2 = Chrome(service=service)
with open(log_file, "r") as fp:
assert len(fp.readlines()) >= 2 * lines
finally:
driver1.quit()
driver2.quit()
os.remove(log_file)


def test_log_output_as_filename() -> None:
log_file = "chromedriver.log"
service = Service(log_output=log_file)
try:
driver = Chrome(service=service)
with open(log_file, "r") as fp:
assert "Starting ChromeDriver" in fp.readline()
finally:
driver.quit()
os.remove(log_file)


def test_log_output_as_file() -> None:
log_name = "chromedriver.log"
log_file = open(log_name, "w", encoding="utf-8")
service = Service(log_output=log_file)
try:
driver = Chrome(service=service)
time.sleep(1)
with open(log_name, "r") as fp:
assert "Starting ChromeDriver" in fp.readline()
finally:
driver.quit()
log_file.close()
os.remove(log_name)


def test_log_output_as_stdout(capfd) -> None:
service = Service(log_output=subprocess.STDOUT)
driver = Chrome(service=service)

out, err = capfd.readouterr()
assert "Starting ChromeDriver" in out
driver.quit()


def test_log_output_null_default(capfd) -> None:
driver = Chrome()

out, err = capfd.readouterr()
assert "Starting ChromeDriver" not in out
driver.quit()
Loading