From c9915fc7e6617b8c8f0b3103028008e5f37e79d1 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 25 Sep 2017 10:23:16 -0400 Subject: [PATCH] Switch to configurable, scaling approach to Selenium timeouts. Implement different "WAIT_TYPES" and try to sleep for different amounts of time based on what is being waited for. Replace sleep statements in addition to Selenium wait on statements. --- test/galaxy_selenium/has_driver.py | 32 ++++++---- test/galaxy_selenium/navigates_galaxy.py | 60 +++++++++++++++---- test/selenium_tests/framework.py | 10 ++-- test/selenium_tests/test_custom_builds.py | 4 +- test/selenium_tests/test_history_panel.py | 11 ++-- .../test_published_histories_grid.py | 6 +- test/selenium_tests/test_saved_histories.py | 14 ++--- test/selenium_tests/test_workflow_editor.py | 6 +- test/selenium_tests/test_workflow_run.py | 4 +- 9 files changed, 89 insertions(+), 58 deletions(-) diff --git a/test/galaxy_selenium/has_driver.py b/test/galaxy_selenium/has_driver.py index 30b4a4f9e88..9e7346fe486 100644 --- a/test/galaxy_selenium/has_driver.py +++ b/test/galaxy_selenium/has_driver.py @@ -35,66 +35,74 @@ class HasDriver: def assert_selector_absent(self, selector): assert len(self.driver.find_elements_by_css_selector(selector)) == 0 - def wait_for_xpath(self, xpath): + def wait_for_xpath(self, xpath, **kwds): element = self._wait_on( ec.presence_of_element_located((By.XPATH, xpath)), "XPATH selector [%s] to become present" % xpath, + **kwds ) return element - def wait_for_xpath_visible(self, xpath): + def wait_for_xpath_visible(self, xpath, **kwds): element = self._wait_on( ec.visibility_of_element_located((By.XPATH, xpath)), "XPATH selector [%s] to become visible" % xpath, + **kwds ) return element - def wait_for_selector(self, selector): + def wait_for_selector(self, selector, **kwds): element = self._wait_on( ec.presence_of_element_located((By.CSS_SELECTOR, selector)), "CSS selector [%s] to become present" % selector, + **kwds ) return element - def wait_for_selector_visible(self, selector): + def wait_for_selector_visible(self, selector, **kwds): element = self._wait_on( ec.visibility_of_element_located((By.CSS_SELECTOR, selector)), "CSS selector [%s] to become visible" % selector, + **kwds ) return element - def wait_for_selector_clickable(self, selector): + def wait_for_selector_clickable(self, selector, **kwds): element = self._wait_on( ec.element_to_be_clickable((By.CSS_SELECTOR, selector)), "CSS selector [%s] to become clickable" % selector, + **kwds ) return element - def wait_for_selector_absent_or_hidden(self, selector): + def wait_for_selector_absent_or_hidden(self, selector, **kwds): element = self._wait_on( ec.invisibility_of_element_located((By.CSS_SELECTOR, selector)), "CSS selector [%s] to become absent or hidden" % selector, + **kwds ) return element - def wait_for_selector_absent(self, selector): + def wait_for_selector_absent(self, selector, **kwds): element = self._wait_on( lambda driver: len(driver.find_elements_by_css_selector(selector)) == 0, "CSS selector [%s] to become absent" % selector, + **kwds ) return element - def wait_for_id(self, id): + def wait_for_id(self, id, **kwds): return self._wait_on( ec.presence_of_element_located((By.ID, id)), "presence of DOM ID [%s]" % id, + **kwds ) - def _wait_on(self, condition, on_str=None): + def _wait_on(self, condition, on_str=None, **kwds): if on_str is None: on_str = str(condition) message = "Timeout waiting on %s." % on_str - wait = self.wait() + wait = self.wait(**kwds) return wait.until(condition, message) def action_chains(self): @@ -106,9 +114,9 @@ class HasDriver: def send_escape(self, element): element.send_keys(Keys.ESCAPE) - def wait(self, timeout=UNSPECIFIED_TIMEOUT): + def wait(self, timeout=UNSPECIFIED_TIMEOUT, **kwds): if timeout is UNSPECIFIED_TIMEOUT: - timeout = self.default_timeout + timeout = self.timeout_for(**kwds) return WebDriverWait(self.driver, timeout) def click_xpath(self, xpath): diff --git a/test/galaxy_selenium/navigates_galaxy.py b/test/galaxy_selenium/navigates_galaxy.py index b0abd72f813..0a90a39ae31 100644 --- a/test/galaxy_selenium/navigates_galaxy.py +++ b/test/galaxy_selenium/navigates_galaxy.py @@ -4,6 +4,7 @@ Implementer must provide a self.build_url method to target Galaxy. """ from __future__ import print_function +import collections import contextlib import random import string @@ -13,6 +14,8 @@ from functools import partial, wraps import requests import yaml +from galaxy.util.bunch import Bunch + from . import sizzle from .data import NAVIGATION_DATA from .has_driver import ( @@ -28,6 +31,26 @@ DEFAULT_PASSWORD = '123456' RETRY_DURING_TRANSITIONS_SLEEP_DEFAULT = .1 RETRY_DURING_TRANSITIONS_ATTEMPTS_DEFAULT = 10 +WaitType = collections.namedtuple("WaitType", ["name", "default_length"]) + +# Default wait times should make sense for a development server under low +# load. Wait times for production servers can be scaled up with a multiplier. +WAIT_TYPES = Bunch( + # Rendering a form and registering callbacks, etc... + UX_RENDER=WaitType("ux_render", 1), + # Fade in, fade out, etc... + UX_TRANSITION=WaitType("ux_transition", 5), + # Toastr popup and dismissal, etc... + UX_POPUP=WaitType("ux_popup", 10), + # Creating a new history and loading it into the panel. + DATABASE_OPERATION=WaitType("database_operation", 10), + # Wait time for jobs to complete in default environment. + JOB_COMPLETION=WaitType("job_completion", 30), +) + +# Choose a moderate wait type for operations that don't specify a type. +DEFAULT_WAIT_TYPE = WAIT_TYPES.DATABASE_OPERATION + class NullTourCallback(object): @@ -95,6 +118,7 @@ class NavigatesGalaxy(HasDriver): """ default_password = DEFAULT_PASSWORD + wait_types = WAIT_TYPES def get(self, url=""): full_url = self.build_url(url) @@ -104,6 +128,16 @@ class NavigatesGalaxy(HasDriver): def navigation_data(self): return NAVIGATION_DATA + def wait_length(self, wait_type): + return wait_type.default_length * self.timeout_multiplier + + def sleep_for(self, wait_type): + time.sleep(self.wait_length(wait_type)) + + def timeout_for(self, **kwds): + wait_type = kwds.get("wait_type", DEFAULT_WAIT_TYPE) + return self.wait_length(wait_type) + def home(self): self.get() self.wait_for_selector_visible("#masthead") @@ -166,7 +200,7 @@ class NavigatesGalaxy(HasDriver): assert len(history_contents) > 0 return history_contents[-1] - def wait_for_history(self, timeout=30, assert_ok=True): + def wait_for_history(self, assert_ok=True): def history_becomes_terminal(driver): current_history_id = self.current_history_id() state = self.api_get("histories/%s" % current_history_id)["state"] @@ -175,21 +209,23 @@ class NavigatesGalaxy(HasDriver): else: return None - final_state = self.wait(timeout).until(history_becomes_terminal) + timeout = self.timeout_for(wait_type=WAIT_TYPES.JOB_COMPLETION) + final_state = self.wait(timeout=timeout).until(history_becomes_terminal) if assert_ok: assert final_state == "ok", final_state return final_state - def history_panel_wait_for_hid_ok(self, hid, timeout=60, allowed_force_refreshes=0): - self.history_panel_wait_for_hid_state(hid, 'ok', timeout=timeout, allowed_force_refreshes=allowed_force_refreshes) + def history_panel_wait_for_hid_ok(self, hid, allowed_force_refreshes=0): + self.history_panel_wait_for_hid_state(hid, 'ok', allowed_force_refreshes=allowed_force_refreshes) - def history_panel_wait_for_hid_visible(self, hid, timeout=60, allowed_force_refreshes=0): + def history_panel_wait_for_hid_visible(self, hid, allowed_force_refreshes=0): current_history_id = self.current_history_id() def history_has_hid(driver): contents = self.api_get("histories/%s/contents" % current_history_id) return any([d for d in contents if d["hid"] == hid]) + timeout = self.timeout_for(wait_type=WAIT_TYPES.JOB_COMPLETION) self.wait(timeout).until(history_has_hid) contents = self.api_get("histories/%s/contents" % current_history_id) history_item = [d for d in contents if d["hid"] == hid][0] @@ -209,7 +245,7 @@ class NavigatesGalaxy(HasDriver): attempt = 0 while True: try: - rval = self.wait_for_selector_visible(history_item_selector) + rval = self.wait_for_selector_visible(history_item_selector, wait_type=WAIT_TYPES.JOB_COMPLETION) break except self.TimeoutException: if attempt >= allowed_force_refreshes: @@ -223,18 +259,18 @@ class NavigatesGalaxy(HasDriver): # Use the search box showing up as a proxy that the history display # has left the "loading" state and is showing a valid set of history contents # (even if empty). - self.wait_for_selector_visible("#current-history-panel input.search-query") + self.wait_for_selector_visible("#current-history-panel input.search-query", wait_type=WAIT_TYPES.DATABASE_OPERATION) - def history_panel_wait_for_hid_hidden(self, hid, timeout=60): + def history_panel_wait_for_hid_hidden(self, hid): current_history_id = self.current_history_id() contents = self.api_get("histories/%s/contents" % current_history_id) history_item = [d for d in contents if d["hid"] == hid][0] history_item_selector = "#%s-%s" % (history_item["history_content_type"], history_item["id"]) - self.wait_for_selector_absent(history_item_selector) + self.wait_for_selector_absent(history_item_selector, wait_type=WAIT_TYPES.JOB_COMPLETION) return history_item_selector - def history_panel_wait_for_hid_state(self, hid, state, timeout=60, allowed_force_refreshes=0): - history_item_selector = self.history_panel_wait_for_hid_visible(hid, timeout=timeout, allowed_force_refreshes=allowed_force_refreshes) + def history_panel_wait_for_hid_state(self, hid, state, allowed_force_refreshes=0): + history_item_selector = self.history_panel_wait_for_hid_visible(hid, allowed_force_refreshes=allowed_force_refreshes) history_item_selector_state = "%s.state-%s" % (history_item_selector, state) try: self.history_item_wait_for_selector(history_item_selector_state, allowed_force_refreshes) @@ -509,7 +545,7 @@ class NavigatesGalaxy(HasDriver): menu_element = self.workflow_editor_options_menu_element() option_elements = menu_element.find_elements_by_css_selector("a") assert len(option_elements) > 0, "Failed to find workflow editor options" - time.sleep(1) + self.sleep_for(WAIT_TYPES.UX_RENDER) found_option = False for option_element in option_elements: if option_label in option_element.text: diff --git a/test/selenium_tests/framework.py b/test/selenium_tests/framework.py index e25726d49bf..21131f7d7bb 100644 --- a/test/selenium_tests/framework.py +++ b/test/selenium_tests/framework.py @@ -5,7 +5,6 @@ from __future__ import print_function import datetime import json import os -import time import traceback import unittest from functools import partial, wraps @@ -30,7 +29,7 @@ from galaxy_selenium import ( # noqa: I100 from galaxy_selenium.navigates_galaxy import NavigatesGalaxy, retry_during_transitions # noqa: I100 from galaxy.util import asbool -DEFAULT_WAIT_TIMEOUT = 60 +DEFAULT_TIMEOUT_MULTIPLIER = 1 DEFAULT_TEST_ERRORS_DIRECTORY = os.path.abspath("database/test_errors") DEFAULT_SELENIUM_BROWSER = "auto" DEFAULT_SELENIUM_REMOTE = False @@ -38,6 +37,7 @@ DEFAULT_SELENIUM_REMOTE_PORT = "4444" DEFAULT_SELENIUM_REMOTE_HOST = "127.0.0.1" DEFAULT_SELENIUM_HEADLESS = "auto" +TIMEOUT_MULTIPLIER = float(os.environ.get("GALAXY_TEST_TIMEOUT_MULTIPLIER", DEFAULT_TIMEOUT_MULTIPLIER)) GALAXY_TEST_ERRORS_DIRECTORY = os.environ.get("GALAXY_TEST_ERRORS_DIRECTORY", DEFAULT_TEST_ERRORS_DIRECTORY) # Test browser can be ["CHROME", "FIREFOX", "OPERA", "PHANTOMJS"] GALAXY_TEST_SELENIUM_BROWSER = os.environ.get("GALAXY_TEST_SELENIUM_BROWSER", DEFAULT_SELENIUM_BROWSER) @@ -196,8 +196,8 @@ class SeleniumTestCase(FunctionalTestCase, NavigatesGalaxy): return default_web_host_for_selenium_tests() @property - def default_timeout(self): - return DEFAULT_WAIT_TIMEOUT + def timeout_multiplier(self): + return TIMEOUT_MULTIPLIER def build_url(self, url, for_selenium=True): if for_selenium: @@ -312,7 +312,7 @@ class UsesHistoryItemAssertions: def _assert_item_button(self, buttons_area, expected_button, button_def): selector = button_def["selector"] # Let old tooltip expire, etc... - time.sleep(1) + self.sleep_for(self.wait_types.UX_TRANSITION) button_item = self.wait_for_selector_visible("%s %s" % (buttons_area, selector)) expected_tooltip = button_def.get("tooltip") self.assert_tooltip_text(button_item, expected_tooltip) diff --git a/test/selenium_tests/test_custom_builds.py b/test/selenium_tests/test_custom_builds.py index f15cef7b4df..d70464888e7 100644 --- a/test/selenium_tests/test_custom_builds.py +++ b/test/selenium_tests/test_custom_builds.py @@ -1,5 +1,3 @@ -import time - from .framework import ( retry_assertion_during_transitions, selenium_test, @@ -81,7 +79,7 @@ class CustomBuildsTestcase(SharedStateSeleniumTestCase): delete_button.click() def get_custom_builds(self): - time.sleep(1) + self.sleep_for(self.wait_types.UX_RENDER) builds = [] grid = self.wait_for_selector('table.grid > tbody') for row in grid.find_elements_by_tag_name('tr'): diff --git a/test/selenium_tests/test_history_panel.py b/test/selenium_tests/test_history_panel.py index 740a5be20f0..697db7a9750 100644 --- a/test/selenium_tests/test_history_panel.py +++ b/test/selenium_tests/test_history_panel.py @@ -1,5 +1,3 @@ -import time - from .framework import ( selenium_test, SeleniumTestCase @@ -70,21 +68,20 @@ class HistoryPanelTestCase(SeleniumTestCase): self.assert_selector_absent_or_hidden(anno_area_selector) tag_icon.click() - time.sleep(.5) + self.sleep_for(self.wait_types.UX_TRANSITION) annon_icon.click() self.wait_for_selector(anno_area_selector) self.assert_selector_absent_or_hidden(tag_area_selector) annon_icon.click() - time.sleep(.5) + self.sleep_for(self.wait_types.UX_TRANSITION) self.assert_selector_absent_or_hidden(tag_area_selector) self.assert_selector_absent_or_hidden(anno_area_selector) @selenium_test def test_refresh_preserves_state(self): - refresh_wait = .5 self.register() self.perform_upload(self.get_filename("1.txt")) self.wait_for_history() @@ -95,7 +92,7 @@ class HistoryPanelTestCase(SeleniumTestCase): self.history_panel_refresh_click() # After the refresh, verify the details are still open. - time.sleep(refresh_wait) + self.sleep_for(self.wait_types.UX_TRANSITION) self.wait_for_selector_clickable(self.history_panel_item_selector(hid=1)) assert self.history_panel_item_showing_details(hid=1) @@ -103,6 +100,6 @@ class HistoryPanelTestCase(SeleniumTestCase): self.history_panel_click_item_title(hid=1, wait=True) assert not self.history_panel_item_showing_details(hid=1) self.history_panel_refresh_click() - time.sleep(refresh_wait) + self.sleep_for(self.wait_types.UX_TRANSITION) self.wait_for_selector_clickable(self.history_panel_item_selector(hid=1)) assert not self.history_panel_item_showing_details(hid=1) diff --git a/test/selenium_tests/test_published_histories_grid.py b/test/selenium_tests/test_published_histories_grid.py index 7cf86e06884..85c6a654567 100644 --- a/test/selenium_tests/test_published_histories_grid.py +++ b/test/selenium_tests/test_published_histories_grid.py @@ -1,5 +1,3 @@ -import time - from .framework import ( retry_assertion_during_transitions, selenium_test, @@ -98,7 +96,7 @@ class HistoryGridTestCase(SharedStateSeleniumTestCase): self.assert_grid_histories_are([self.history1_name, self.history3_name], False) def get_histories(self, sleep=False): - time.sleep(1.5) + self.sleep_for(self.wait_types.UX_RENDER) names = [] grid = self.wait_for_selector('#grid-table-body') for row in grid.find_elements_by_tag_name('tr'): @@ -146,7 +144,7 @@ class HistoryGridTestCase(SharedStateSeleniumTestCase): close_link_selector = 'a[filter_key="%s"][filter_val="%s"]' % \ (filter_key, filter_value) self.wait_for_and_click_selector(close_link_selector) - time.sleep(.5) + self.sleep_for(self.wait_types.UX_RENDER) def set_annotation(self, annotation): anno_icon_selector = self.test_data['historyPanel']['selectors']['history']['annoIcon'] diff --git a/test/selenium_tests/test_saved_histories.py b/test/selenium_tests/test_saved_histories.py index 5dae8bb0706..a05d6946699 100644 --- a/test/selenium_tests/test_saved_histories.py +++ b/test/selenium_tests/test_saved_histories.py @@ -1,5 +1,3 @@ -import time - from .framework import ( retry_assertion_during_transitions, retry_during_transitions, @@ -21,7 +19,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self._login() self.navigate_to_saved_histories_page() self.click_popup_option(self.history2_name, 'Switch') - time.sleep(.5) + self.sleep_for(self.wait_types.UX_RENDER) @retry_assertion_during_transitions def assert_history_name_switched(): @@ -50,7 +48,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.histories_click_advanced_search() self.select_filter('sharing', 'published') - time.sleep(1) + self.sleep_for(self.wait_types.UX_RENDER) self.assert_histories_in_grid([self.history2_name]) @@ -84,7 +82,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.histories_click_advanced_search() self.select_filter('deleted', 'True') - time.sleep(1) + self.sleep_for(self.wait_types.UX_RENDER) # Restore the history self.click_popup_option(self.history2_name, 'Undelete') @@ -129,7 +127,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.histories_click_advanced_search() self.select_filter('deleted', 'True') - time.sleep(1) + self.sleep_for(self.wait_types.UX_RENDER) # Restore multiple histories self.check_histories([self.history2_name, self.history3_name]) @@ -253,7 +251,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): @retry_during_transitions def get_histories(self): - time.sleep(.5) + self.sleep_for(self.wait_types.UX_RENDER) names = [] grid = self.wait_for_selector('#grid-table-body') for row in grid.find_elements_by_tag_name('tr'): @@ -272,7 +270,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): close_button_selector = 'a[filter_key="%s"][filter_val="%s"]' % \ (filter_key, filter_value) self.wait_for_and_click_selector(close_button_selector) - time.sleep(.5) + self.sleep_for(self.wait_types.UX_RENDER) def navigate_to_saved_histories_page(self): self.home() diff --git a/test/selenium_tests/test_workflow_editor.py b/test/selenium_tests/test_workflow_editor.py index d44d252bd03..1d053a56edc 100644 --- a/test/selenium_tests/test_workflow_editor.py +++ b/test/selenium_tests/test_workflow_editor.py @@ -1,5 +1,3 @@ -import time - from ._workflow_fixtures import ( WORKFLOW_SIMPLE_CAT_TWICE, WORKFLOW_WITH_INVALID_STATE, @@ -27,7 +25,7 @@ class WorkflowEditorTestCase(SeleniumTestCase): def test_data_input(self): self.workflow_create_new() menu = self.wait_for_selector(".toolMenu") - time.sleep(1) + self.sleep_for(self.wait_types.UX_RENDER) inputs_section = menu.find_element_by_css_selector("#title___workflow__inputs__") inputs_link = inputs_section.find_element_by_css_selector("a span") inputs_link.click() @@ -43,7 +41,7 @@ class WorkflowEditorTestCase(SeleniumTestCase): workflow_populator.upload_yaml_workflow(WORKFLOW_SIMPLE_CAT_TWICE) self.workflow_index_open() self.workflow_index_click_option("Edit") - time.sleep(1) + self.sleep_for(self.wait_types.UX_RENDER) self.workflow_editor_click_option("Save As") @selenium_test diff --git a/test/selenium_tests/test_workflow_run.py b/test/selenium_tests/test_workflow_run.py index 8bdc834f7b2..fed8a348b6b 100644 --- a/test/selenium_tests/test_workflow_run.py +++ b/test/selenium_tests/test_workflow_run.py @@ -1,5 +1,3 @@ -import time - from ._workflow_fixtures import ( WORKFLOW_SIMPLE_CAT_TWICE, WORKFLOW_WITH_OLD_TOOL_VERSION, @@ -38,6 +36,6 @@ class WorkflowRunTestCase(SeleniumTestCase, UsesHistoryItemAssertions): self.workflow_index_open() self.workflow_index_click_option("Run") - time.sleep(.5) + self.sleep_for(self.wait_types.UX_TRANSITION) # Check that this tool form contains a warning about different versions. self.assert_warning_message(contains="different versions")