diff --git a/.gitignore b/.gitignore index 36662aba..58193f90 100644 --- a/.gitignore +++ b/.gitignore @@ -45,5 +45,5 @@ config/dev.ini # coverage output .coverage -# selenium +# selenium testing browserstacklocal diff --git a/config/selenium.ini b/config/selenium.ini index 81029be6..5ad48446 100644 --- a/config/selenium.ini +++ b/config/selenium.ini @@ -1,5 +1,4 @@ [default] PGDATABASE = atat_selenium -REDIS_URI = redis://redishost:6379 CRL_DIRECTORY = tests/fixtures/crl WTF_CSRF_ENABLED = false diff --git a/pytest.ini b/pytest.ini index 91d52803..30767796 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,4 +2,4 @@ norecursedirs = .venv .git node_modules env = D:FLASK_ENV=test -addopts = --cov=atst --cov-report term --cov-fail-under 90 +addopts = --ignore=tests/acceptance/ --cov=atst --cov-report term --cov-fail-under 90 diff --git a/script/selenium_test b/script/selenium_test index 3132a23a..fcf3515a 100755 --- a/script/selenium_test +++ b/script/selenium_test @@ -45,8 +45,8 @@ echo "starting BrowserStack local client..." BSL_ID=$! # run example selenium script that fetches the home page -echo "running selenium example script" -pipenv run python selenium_example.py +echo "running selenium tests" +pipenv run pytest tests/acceptance -s # kill BrowserStackLocal kill $BSL_ID diff --git a/selenium_example.py b/selenium_example.py deleted file mode 100644 index d7582aea..00000000 --- a/selenium_example.py +++ /dev/null @@ -1,26 +0,0 @@ -import os -from selenium import webdriver -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities - -desired_cap = { - 'browser': 'IE', - 'browser_version': '10.0', - 'os': 'Windows', - 'os_version': '7', - 'resolution': '1024x768', - 'browserstack.local': True -} - -print("initializing the webdriver") -driver = webdriver.Remote( - command_executor='http://{}:{}@hub.browserstack.com:80/wd/hub'.format(os.getenv("BROWSERSTACK_EMAIL"), os.getenv("BROWSERSTACK_TOKEN")), - desired_capabilities=desired_cap) - -print("fetching the localhost page") -driver.get("http://localhost:8000") -if not "JEDI" in driver.title: - raise Exception("NO JEDI") -print("this is the page title: {}".format(driver.title)) -driver.quit() -print("exiting") diff --git a/tests/acceptance/__init__.py b/tests/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/acceptance/conftest.py b/tests/acceptance/conftest.py new file mode 100644 index 00000000..b19b9855 --- /dev/null +++ b/tests/acceptance/conftest.py @@ -0,0 +1,67 @@ +import os +import pytest +import logging +from logging.handlers import RotatingFileHandler +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + +from .live_server import LiveServer + + +@pytest.fixture(scope="session") +def live_app(app): + handler = RotatingFileHandler('log/acceptance.log', maxBytes=10000, backupCount=1) + handler.setLevel(logging.INFO) + app.logger.addHandler(handler) + + runnable = LiveServer(app, port=8943, timeout=10) + runnable.spawn_live_server() + app.server_url = runnable.server_url + + yield app + + runnable.terminate() + + +@pytest.fixture(scope="session") +def browserstack_config(): + return { + "win7_ie10": { + "browser": "IE", + "browser_version": "10.0", + "os": "Windows", + "os_version": "7", + "resolution": "1024x768", + "browserstack.local": True, + }, + "iphone7": { + 'browserName': 'iPhone', + 'device': 'iPhone 7', + 'realMobile': 'true', + 'os_version': '10.3', + "browserstack.local": True, + } + } + + +@pytest.fixture(scope="session") +def driver_builder(browserstack_config): + def build_driver(config_key): + return webdriver.Remote( + command_executor="http://{}:{}@hub.browserstack.com:80/wd/hub".format( + os.getenv("BROWSERSTACK_EMAIL"), os.getenv("BROWSERSTACK_TOKEN") + ), + desired_capabilities=browserstack_config.get(config_key), + ) + + return build_driver + + +@pytest.fixture(scope="session") +def ie10_driver(driver_builder): + driver = driver_builder("win7_ie10") + + yield driver + + driver.quit() diff --git a/tests/acceptance/live_server.py b/tests/acceptance/live_server.py new file mode 100644 index 00000000..2dd46baf --- /dev/null +++ b/tests/acceptance/live_server.py @@ -0,0 +1,109 @@ +import gc +import multiprocessing +import socket +import socketserver +import time +from urllib.parse import urlparse, urljoin + +# This is adapted from flask-testing, https://github.com/jarus/flask-testing +# Inspired by https://docs.djangoproject.com/en/dev/topics/testing/#django.test.LiveServerTestCase +class LiveServer: + def __init__(self, app, port=5000, timeout=5): + self.app = app + self._configured_port = port + self._timeout = timeout + self._port_value = multiprocessing.Value("i", self._configured_port) + + @property + def server_url(self): + return "http://localhost:%s" % self._port_value.value + + def spawn_live_server(self): + self._process = None + port_value = self._port_value + + def worker(app, port): + # Based on solution: http://stackoverflow.com/a/27598916 + # Monkey-patch the server_bind so we can determine the port bound + # by Flask. This handles the case where the port specified is `0`, + # which means that the OS chooses the port. This is the only known + # way (currently) of getting the port out of Flask once we call + # `run`. + original_socket_bind = socketserver.TCPServer.server_bind + + def socket_bind_wrapper(self): + ret = original_socket_bind(self) + + # Get the port and save it into the port_value, so the parent + # process can read it. + (_, port) = self.socket.getsockname() + port_value.value = port + socketserver.TCPServer.server_bind = original_socket_bind + return ret + + socketserver.TCPServer.server_bind = socket_bind_wrapper + app.run(port=port, use_reloader=False) + + self._process = multiprocessing.Process( + target=worker, args=(self.app, self._configured_port) + ) + + self._process.start() + + # We must wait for the server to start listening, but give up + # after a specified maximum timeout + start_time = time.time() + + while True: + elapsed_time = time.time() - start_time + if elapsed_time > self._timeout: + raise RuntimeError( + "Failed to start the server after %d seconds. " % self._timeout + ) + + if self._can_ping_server(): + break + + def _can_ping_server(self): + host, port = self.address + if port == 0: + # Port specified by the user was 0, and the OS has not yet assigned + # the proper port. + return False + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect((host, port)) + except socket.error as e: + success = False + else: + success = True + finally: + sock.close() + + return success + + @property + def address(self): + """ + Gets the server address used to test the connection with a socket. + Respects both the LIVESERVER_PORT config value and overriding server_url + """ + parts = urlparse(self.server_url) + + host = parts.hostname + port = parts.port + + if port is None: + if parts.scheme == "http": + port = 80 + elif parts.scheme == "https": + port = 443 + else: + raise RuntimeError("Unsupported server url scheme: %s" % parts.scheme) + + return host, port + + def terminate(self): + if self._process: + self._process.terminate() diff --git a/tests/acceptance/test_basic.py b/tests/acceptance/test_basic.py new file mode 100644 index 00000000..1c549596 --- /dev/null +++ b/tests/acceptance/test_basic.py @@ -0,0 +1,3 @@ +def test_can_get_title(live_app, ie10_driver): + ie10_driver.get(live_app.server_url) + assert "JEDI" in ie10_driver.title