"""Basis for Selenium test framework.""" from __future__ import absolute_import from __future__ import print_function import datetime import json import os import traceback import unittest from functools import partial, wraps import requests from gxformat2 import ( convert_and_import_workflow, ImporterGalaxyInterface, ) try: from pyvirtualdisplay import Display except ImportError: Display = None from six.moves.urllib.parse import urljoin from base import populators # noqa: I100,I202 from base.api import UsesApiTestCaseMixin # noqa: I100 from base.driver_util import classproperty, DEFAULT_WEB_HOST, get_ip_address # noqa: I100 from base.testcase import FunctionalTestCase # noqa: I100 from galaxy.selenium import ( # noqa: I100,I201 driver_factory, ) from galaxy.selenium.navigates_galaxy import ( # noqa: I100 NavigatesGalaxy, retry_during_transitions ) from galaxy.util import asbool # noqa: I201 DEFAULT_TIMEOUT_MULTIPLIER = 1 DEFAULT_TEST_ERRORS_DIRECTORY = os.path.abspath("database/test_errors") DEFAULT_SELENIUM_BROWSER = "auto" DEFAULT_SELENIUM_REMOTE = False DEFAULT_SELENIUM_REMOTE_PORT = "4444" DEFAULT_SELENIUM_REMOTE_HOST = "127.0.0.1" DEFAULT_SELENIUM_HEADLESS = "auto" DEFAULT_ADMIN_USER = "test@bx.psu.edu" DEFAULT_ADMIN_PASSWORD = "testpass" 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) GALAXY_TEST_SCREENSHOTS_DIRECTORY = os.environ.get("GALAXY_TEST_SCREENSHOTS_DIRECTORY", None) # Test browser can be ["CHROME", "FIREFOX", "OPERA", "PHANTOMJS"] GALAXY_TEST_SELENIUM_BROWSER = os.environ.get("GALAXY_TEST_SELENIUM_BROWSER", DEFAULT_SELENIUM_BROWSER) GALAXY_TEST_SELENIUM_REMOTE = os.environ.get("GALAXY_TEST_SELENIUM_REMOTE", DEFAULT_SELENIUM_REMOTE) GALAXY_TEST_SELENIUM_REMOTE_PORT = os.environ.get("GALAXY_TEST_SELENIUM_REMOTE_PORT", DEFAULT_SELENIUM_REMOTE_PORT) GALAXY_TEST_SELENIUM_REMOTE_HOST = os.environ.get("GALAXY_TEST_SELENIUM_REMOTE_HOST", DEFAULT_SELENIUM_REMOTE_HOST) GALAXY_TEST_SELENIUM_HEADLESS = os.environ.get("GALAXY_TEST_SELENIUM_HEADLESS", DEFAULT_SELENIUM_HEADLESS) GALAXY_TEST_EXTERNAL_FROM_SELENIUM = os.environ.get("GALAXY_TEST_EXTERNAL_FROM_SELENIUM", None) # Auto-retry selenium tests this many times. GALAXY_TEST_SELENIUM_RETRIES = int(os.environ.get("GALAXY_TEST_SELENIUM_RETRIES", "0")) GALAXY_TEST_SELENIUM_USER_EMAIL = os.environ.get("GALAXY_TEST_SELENIUM_USER_EMAIL", None) GALAXY_TEST_SELENIUM_USER_PASSWORD = os.environ.get("GALAXY_TEST_SELENIUM_USER_PASSWORD", None) GALAXY_TEST_SELENIUM_ADMIN_USER_EMAIL = os.environ.get("GALAXY_TEST_SELENIUM_ADMIN_USER_EMAIL", DEFAULT_ADMIN_USER) GALAXY_TEST_SELENIUM_ADMIN_USER_PASSWORD = os.environ.get("GALAXY_TEST_SELENIUM_ADMIN_USER_PASSWORD", DEFAULT_ADMIN_PASSWORD) # JS code to execute in Galaxy JS console to setup localStorage of session for logging and # logging "flatten" messages because it seems Selenium (with Chrome at least) only grabs # the first argument to console.XXX when recovering the browser log. SETUP_LOGGING_JS = ''' window.localStorage && window.localStorage.setItem("galaxy:debug", true); window.localStorage && window.localStorage.setItem("galaxy:debug:flatten", true); ''' try: from nose.tools import nottest except ImportError: def nottest(x): return x def managed_history(f): """Ensure a Selenium test has a distinct, named history. Cleanup the history after the job is complete as well unless GALAXY_TEST_NO_CLEANUP is set in the environment. """ @wraps(f) def func_wrapper(self, *args, **kwds): self.home() history_name = f.__name__ + datetime.datetime.now().strftime("%Y%m%d%H%M%s") self.history_panel_create_new_with_name(history_name) try: f(self, *args, **kwds) finally: if "GALAXY_TEST_NO_CLEANUP" not in os.environ: try: current_history_id = self.current_history_id() self.dataset_populator.cancel_history_jobs(current_history_id) self.api_delete("histories/%s" % current_history_id) except Exception: print("Faild to cleanup managed history, selenium connection corrupted somehow?") return func_wrapper def dump_test_information(self, name_prefix): if GALAXY_TEST_ERRORS_DIRECTORY and GALAXY_TEST_ERRORS_DIRECTORY != "0": if not os.path.exists(GALAXY_TEST_ERRORS_DIRECTORY): os.makedirs(GALAXY_TEST_ERRORS_DIRECTORY) result_name = name_prefix + datetime.datetime.now().strftime("%Y%m%d%H%M%s") target_directory = os.path.join(GALAXY_TEST_ERRORS_DIRECTORY, result_name) def write_file(name, content, raw=False): with open(os.path.join(target_directory, name), "wb") as buf: buf.write(content.encode("utf-8") if not raw else content) os.makedirs(target_directory) write_file("stacktrace.txt", traceback.format_exc()) for snapshot in getattr(self, "snapshots", []): snapshot.write_to_error_directory(write_file) # Try to use the Selenium driver to recover more debug information, but don't # throw an exception if the connection is broken in some way. try: self.driver.save_screenshot(os.path.join(target_directory, "last.png")) write_file("page_source.txt", self.driver.page_source) write_file("DOM.txt", self.driver.execute_script("return document.documentElement.outerHTML")) except Exception: print("Failed to use test driver to recover debug information from Selenium.") write_file("selenium_exception.txt", traceback.format_exc()) for log_type in ["browser", "driver"]: try: full_log = self.driver.get_log(log_type) trimmed_log = [l for l in full_log if l["level"] not in ["DEBUG", "INFO"]] write_file("%s.log.json" % log_type, json.dumps(trimmed_log, indent=True)) write_file("%s.log.verbose.json" % log_type, json.dumps(full_log, indent=True)) except Exception: continue @nottest def selenium_test(f): test_name = f.__name__ @wraps(f) def func_wrapper(self, *args, **kwds): retry_attempts = 0 while True: if retry_attempts > 0: self.reset_driver_and_session() try: return f(self, *args, **kwds) except unittest.SkipTest: dump_test_information(self, test_name) # Don't retry if we have purposely decided to skip the test. raise except Exception: dump_test_information(self, test_name) if retry_attempts < GALAXY_TEST_SELENIUM_RETRIES: retry_attempts += 1 print("Test function [%s] threw an exception, retrying. Failed attempts - %s." % (test_name, retry_attempts)) else: raise return func_wrapper retry_assertion_during_transitions = partial(retry_during_transitions, exception_check=lambda e: isinstance(e, AssertionError)) class TestSnapshot(object): def __init__(self, driver, index, description): self.screenshot_binary = driver.get_screenshot_as_png() self.description = description self.index = index self.exc = traceback.format_exc() self.stack = traceback.format_stack() def write_to_error_directory(self, write_file_func): prefix = "%d-%s" % (self.index, self.description) write_file_func("%s-screenshot.png" % prefix, self.screenshot_binary, raw=True) write_file_func("%s-traceback.txt" % prefix, self.exc) write_file_func("%s-stack.txt" % prefix, str(self.stack)) class SeleniumTestCase(FunctionalTestCase, NavigatesGalaxy, UsesApiTestCaseMixin): # If run one-off via nosetests, the next line ensures test # tools and datatypes are used instead of configured tools. framework_tool_and_types = True # Override this in subclasses to ensure a user is logged in # before each test. If GALAXY_TEST_SELENIUM_USER_EMAIL and # GALAXY_TEST_SELENIUM_USER_PASSWORD are set these values # will be used to login. ensure_registered = False requires_admin = False def setUp(self): super(SeleniumTestCase, self).setUp() # Deal with the case when Galaxy has a different URL when being accessed by Selenium # then when being accessed by local API calls. if GALAXY_TEST_EXTERNAL_FROM_SELENIUM is not None: self.target_url_from_selenium = GALAXY_TEST_EXTERNAL_FROM_SELENIUM else: self.target_url_from_selenium = self.url self.snapshots = [] self.setup_driver_and_session() if self.requires_admin and GALAXY_TEST_SELENIUM_ADMIN_USER_EMAIL == DEFAULT_ADMIN_USER: self._setup_interactor() self._setup_user(GALAXY_TEST_SELENIUM_ADMIN_USER_EMAIL) self._try_setup_with_driver() def _try_setup_with_driver(self): try: self.setup_with_driver() except Exception: dump_test_information(self, self.__class__.__name__ + "_setup") raise def setup_with_driver(self): """Override point that allows setting up data using self.driver and Selenium connection. Overriding this instead of setUp will ensure debug data such as screenshots and stack traces are dumped if there are problems with the setup and it will be re-ran on test retries. """ def tearDown(self): exception = None try: super(SeleniumTestCase, self).tearDown() except Exception as e: exception = e try: self.tear_down_driver() except Exception as e: exception = e if exception is not None: raise exception def snapshot(self, description): """Create a debug snapshot (DOM, screenshot, etc...) that is written out on tool failure. This information will be automatically written to a per-test directory created for all failed tests. """ self.snapshots.append(TestSnapshot(self.driver, len(self.snapshots), description)) def screenshot(self, label): """If GALAXY_TEST_SCREENSHOTS_DIRECTORY is set create a screenshot there named