Skip to content

Commit

Permalink
feat: add more timeout configuration on screenshots (apache#14868)
Browse files Browse the repository at this point in the history
* feat: more timeout configuration on screenshots

* add tests
  • Loading branch information
dpgaspar authored Jun 8, 2021
1 parent 1af91ed commit 4e998e6
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 22 deletions.
10 changes: 8 additions & 2 deletions superset/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,11 +475,17 @@ def _try_json_readsha( # pylint: disable=unused-argument
"CACHE_NO_NULL_WARNING": True,
}

# Used for thumbnails and other api: Time in seconds before selenium
# Time in seconds before selenium
# times out after trying to locate an element on the page and wait
# for that element to load for an alert screenshot.
# for that element to load for a screenshot.
SCREENSHOT_LOCATE_WAIT = 10
# Time in seconds before selenium
# times out after waiting for all DOM class elements named "loading" are gone.
SCREENSHOT_LOAD_WAIT = 60
# Selenium destroy retries
SCREENSHOT_SELENIUM_RETRIES = 5
# Give selenium an headstart, in seconds
SCREENSHOT_SELENIUM_HEADSTART = 3

# ---------------------------------------------------
# Image and file configuration
Expand Down
37 changes: 19 additions & 18 deletions superset/utils/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
# under the License.

import logging
import time
from time import sleep
from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING

from flask import current_app
from retry.api import retry_call
from selenium.common.exceptions import TimeoutException, WebDriverException
from selenium.common.exceptions import (
StaleElementReferenceException,
TimeoutException,
WebDriverException,
)
from selenium.webdriver import chrome, firefox
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
Expand All @@ -33,11 +37,6 @@
WindowSize = Tuple[int, int]
logger = logging.getLogger(__name__)

# Time in seconds, we will wait for the page to load and render
SELENIUM_CHECK_INTERVAL = 2
SELENIUM_RETRIES = 5
SELENIUM_HEADSTART = 3


if TYPE_CHECKING:
from flask_appbuilder.security.sqla.models import User
Expand Down Expand Up @@ -95,18 +94,17 @@ def destroy(driver: WebDriver, tries: int = 2) -> None:
pass

def get_screenshot(
self,
url: str,
element_name: str,
user: "User",
retries: int = SELENIUM_RETRIES,
self, url: str, element_name: str, user: "User",
) -> Optional[bytes]:

driver = self.auth(user)
driver.set_window_size(*self._window)
driver.get(url)
img: Optional[bytes] = None
logger.debug("Sleeping for %i seconds", SELENIUM_HEADSTART)
time.sleep(SELENIUM_HEADSTART)
selenium_headstart = current_app.config["SCREENSHOT_SELENIUM_HEADSTART"]
logger.debug("Sleeping for %i seconds", selenium_headstart)
sleep(selenium_headstart)

try:
logger.debug("Wait for the presence of %s", element_name)
element = WebDriverWait(driver, self._screenshot_locate_wait).until(
Expand All @@ -120,11 +118,14 @@ def get_screenshot(
img = element.screenshot_as_png
except TimeoutException:
logger.error("Selenium timed out requesting url %s", url, exc_info=True)
except StaleElementReferenceException:
logger.error(
"Selenium timed out while waiting for chart(s) to load %s",
url,
exc_info=True,
)
except WebDriverException as ex:
logger.error(ex, exc_info=True)
# Some webdrivers do not support screenshots for elements.
# In such cases, take a screenshot of the entire page.
img = driver.screenshot() # pylint: disable=no-member
finally:
self.destroy(driver, retries)
self.destroy(driver, current_app.config["SCREENSHOT_SELENIUM_RETRIES"])
return img
46 changes: 44 additions & 2 deletions tests/thumbnails_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import urllib.request
from io import BytesIO
from unittest import skipUnless
from unittest.mock import patch
from unittest.mock import ANY, call, patch

from flask_testing import LiveServerTestCase
from sqlalchemy.sql import func
Expand All @@ -29,7 +29,8 @@
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.utils.screenshots import ChartScreenshot, DashboardScreenshot
from superset.utils.urls import get_url_host
from superset.utils.urls import get_url_host, get_url_path
from superset.utils.webdriver import WebDriverProxy
from tests.conftest import with_feature_flags
from tests.test_app import app

Expand Down Expand Up @@ -61,6 +62,47 @@ def test_get_async_dashboard_screenshot(self):
self.assertEqual(response.getcode(), 202)


class TestWebDriverProxy(SupersetTestCase):
@patch("superset.utils.webdriver.WebDriverWait")
@patch("superset.utils.webdriver.firefox")
@patch("superset.utils.webdriver.sleep")
def test_screenshot_selenium_headstart(
self, mock_sleep, mock_webdriver, mock_webdriver_wait
):
webdriver = WebDriverProxy("firefox")
user = security_manager.get_user_by_username(
app.config["THUMBNAIL_SELENIUM_USER"]
)
url = get_url_path("Superset.slice", slice_id=1, standalone="true")
app.config["SCREENSHOT_SELENIUM_HEADSTART"] = 5
webdriver.get_screenshot(url, "chart-container", user=user)
assert mock_sleep.call_args_list[0] == call(5)

@patch("superset.utils.webdriver.WebDriverWait")
@patch("superset.utils.webdriver.firefox")
def test_screenshot_selenium_locate_wait(self, mock_webdriver, mock_webdriver_wait):
app.config["SCREENSHOT_LOCATE_WAIT"] = 15
webdriver = WebDriverProxy("firefox")
user = security_manager.get_user_by_username(
app.config["THUMBNAIL_SELENIUM_USER"]
)
url = get_url_path("Superset.slice", slice_id=1, standalone="true")
webdriver.get_screenshot(url, "chart-container", user=user)
assert mock_webdriver_wait.call_args_list[0] == call(ANY, 15)

@patch("superset.utils.webdriver.WebDriverWait")
@patch("superset.utils.webdriver.firefox")
def test_screenshot_selenium_load_wait(self, mock_webdriver, mock_webdriver_wait):
app.config["SCREENSHOT_LOAD_WAIT"] = 15
webdriver = WebDriverProxy("firefox")
user = security_manager.get_user_by_username(
app.config["THUMBNAIL_SELENIUM_USER"]
)
url = get_url_path("Superset.slice", slice_id=1, standalone="true")
webdriver.get_screenshot(url, "chart-container", user=user)
assert mock_webdriver_wait.call_args_list[1] == call(ANY, 15)


class TestThumbnails(SupersetTestCase):

mock_image = b"bytes mock image"
Expand Down

0 comments on commit 4e998e6

Please sign in to comment.