Create attractive VASL scenarios, with loads of useful information embedded to assist with game play. https://vasl-templates.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
vasl-templates/conftest.py

255 lines
10 KiB

""" pytest support functions. """
import os
import threading
import json
import re
import tempfile
import logging
import urllib.request
from urllib.error import URLError
import pytest
from flask import url_for
from vasl_templates.webapp import app
from vasl_templates.webapp.tests import utils
from vasl_templates.webapp.tests.control_tests import ControlTests
FLASK_WEBAPP_PORT = 5011
_pytest_options = None
# ---------------------------------------------------------------------
def pytest_addoption( parser ):
"""Configure pytest options."""
# NOTE: This file needs to be in the project root for this to work :-/
# add test options
parser.addoption(
"--webapp", action="store", dest="webapp_url", default=None,
help="Webapp server to test against."
)
# NOTE: Chrome seems to be ~15% faster than Firefox, headless ~5% faster than headful.
parser.addoption(
"--webdriver", action="store", dest="webdriver", default="chrome",
help="Webdriver to use."
)
parser.addoption(
"--headless", action="store_true", dest="headless", default=False,
help="Run the tests headless."
)
parser.addoption(
"--window-size", action="store", dest="window_size", default="1020x700",
help="Browser window size."
)
# add test options
parser.addoption(
"--short-tests", action="store_true", dest="short_tests", default=False,
help="Run a shorter version of the test suite."
)
# NOTE: It's not good to have the code run differently to how it will normally,
# but using the clipboard to retrieve snippets causes more trouble than it's worth :-/
# since any kind of clipboard activity while the tests are running could cause them to fail
# (even when running in a VM, if it's configured to share the physical host's clipboard :wall:).
parser.addoption(
"--use-clipboard", action="store_true", dest="use_clipboard", default=False,
help="Use the clipboard to get snippets."
)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def pytest_configure( config ):
"""Called after command-line options have been parsed."""
global _pytest_options
_pytest_options = config.option
import vasl_templates.webapp.tests
vasl_templates.webapp.tests.pytest_options = config.option
# ---------------------------------------------------------------------
_webapp = None
@pytest.fixture( scope="function" )
def webapp():
"""Launch the webapp."""
# get the global webapp fixture
global _webapp
if _webapp is None:
_webapp = _make_webapp()
# reset the remote webapp server
_webapp.control_tests.start_tests()
# return the webapp to the caller
yield _webapp
# reset the remote webapp server
_webapp.control_tests.end_tests()
def _make_webapp():
"""Create the global webapp fixture."""
# initialize
webapp_url = _pytest_options.webapp_url
if webapp_url and not webapp_url.startswith( "http://" ):
webapp_url = "http://" + webapp_url
app.base_url = webapp_url if webapp_url else "http://localhost:{}".format( FLASK_WEBAPP_PORT )
_disable_console_logging()
# initialize
# WTF?! https://github.com/pallets/flask/issues/824
def make_webapp_url( endpoint, **kwargs ):
"""Generate a webapp URL."""
with app.test_request_context():
# NOTE: When we perform actions at high speed, the notification balloons can build up
# very quickly, causing problems by obscuring other elements and making them non-clickable :-/
# We used to explicitly dismiss them, but it's simpler to just always disable them.
kwargs["store_msgs"] = 1
# stop the browser from checking for a dirty scenario when leaving the page
kwargs["disable_close_window_check"] = 1
# check if the tests are being run headless
if _pytest_options.headless:
# yup - there is no clipboard support :-/
_pytest_options.use_clipboard = False
# check if we should disable using the clipboard for snippets
if not _pytest_options.use_clipboard:
# NOTE: It's not a bad idea to bypass the clipboard, even when running in a browser,
# to avoid problems if something else uses the clipboard while the tests are running.
kwargs["store_clipboard"] = 1
url = url_for( endpoint, _external=True, **kwargs )
url = url.replace( "http://localhost", app.base_url )
return url
app.url_for = make_webapp_url
# check if we need to start a local webapp server
if not webapp_url:
# yup - make it so
# NOTE: We run the server thread as a daemon so that it won't prevent the tests from finishing
# when they're done. We used to call $/shutdown after yielding the webapp fixture, but when
# we changed it from being per-session to per-function, we can no longer do that.
# This means that the webapp doesn't get a chance to shutdown properly (in particular,
# clean up the gRPC service), but since we send an EndTests message at the of each test,
# the remote server gets a chance to clean up then. It's not perfect (e.g. if the tests fail
# or otherwise finish early before they get a chance to send the EndTests message), but
# we can live with it.
thread = threading.Thread(
target = lambda: app.run( host="0.0.0.0", port=FLASK_WEBAPP_PORT, use_reloader=False ),
daemon = True
)
thread.start()
# wait for the server to start up
def is_ready():
"""Try to connect to the webapp server."""
try:
url = app.url_for( "ping" )
with urllib.request.urlopen( url ) as resp:
assert resp.read().startswith( b"pong: " )
return True
except URLError:
return False
except Exception as ex: #pylint: disable=broad-except
assert False, "Unexpected exception: {}".format(ex)
utils.wait_for( 5, is_ready )
# set up control of the remote webapp server
try:
url = app.url_for( "get_control_tests" )
with urllib.request.urlopen( url ) as resp:
resp_data = json.load( resp )
except urllib.error.HTTPError as ex:
if ex.code == 404:
raise RuntimeError( "Can't get the test control port - has remote test control been enabled?" ) from ex
raise
port_no = resp_data.get( "port" )
if not port_no:
raise RuntimeError( "The webapp server is not running the test control service." )
mo = re.search( r"^http://(.+):\d+$", app.base_url )
addr = "{}:{}".format( mo.group(1), port_no )
app.control_tests = ControlTests( addr )
# NOTE: We set the back-end webdriver to be the of the same type (Firefox or Chrome) as the browser
# being used to drive the tests, which, strictly speaking, doesn't make sense, since the two things
# don't have anything to do with each other. However, this is a convenient way to switch the backend
# webdriver's and exercise both of them. The webdriver binary must be on the path, but if it's not,
# we won't have even got this far, since it needs to be there to drive the browser.
# NOTE: This will have no effect if we're talking to a remote server, but we can live with that.
if _pytest_options.webdriver == "firefox":
app.config[ "WEBDRIVER_PATH" ] = "geckodriver"
elif _pytest_options.webdriver == "chrome":
app.config[ "WEBDRIVER_PATH" ] = "chromedriver"
# NOTE: Trumboyg adds a lot of buttons to the UI, which slows Selenium down a lot
# when it's searching for elements, so we run tests with a minimal configuration.
app.config[ "TRUMBOWYG_BUTTONS_VICTORY_CONDITIONS" ] = [ "viewHTML" ]
app.config[ "TRUMBOWYG_BUTTONS_SIMPLE_NOTE_DIALOG" ] = [ "viewHTML" ]
return app
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@pytest.fixture( scope="session" )
def test_client():
"""Return a test client that can be used to connect to the webapp."""
_disable_console_logging()
return app.test_client()
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@pytest.fixture( scope="session" )
def webdriver( request ):
"""Return a webdriver that can be used to control a browser.
It would be nice to be able to drive the embedded browser in the wrapper
Qt app with e.g. this:
https://github.com/cisco-open-source/qtwebdriver
but it only works with the old QtWebKit, which was removed in Qt 5.6 :-/
"""
# initialize
driver = request.config.getoption( "--webdriver" )
from selenium import webdriver as wb
log_fname = os.path.join( tempfile.gettempdir(), "webdriver-pytest.log" )
if driver == "firefox":
options = wb.FirefoxOptions()
options.headless = _pytest_options.headless
service = wb.firefox.service.Service( log_path=log_fname )
driver = wb.Firefox( options=options, service=service )
elif driver == "chrome":
options = wb.ChromeOptions()
options.headless = _pytest_options.headless
options.add_argument( "--disable-gpu" )
driver = wb.Chrome( options=options )
else:
raise RuntimeError( "Unknown webdriver: {}".format( driver ) )
# set the browser size
words = _pytest_options.window_size.split( "x" )
driver.set_window_size( int(words[0]), int(words[1]) )
# return the webdriver to the caller
try:
yield driver
finally:
driver.quit()
# ---------------------------------------------------------------------
def _disable_console_logging():
"""Disable Python logging to the console.
We do this when running tests because:
(1) pytest's output is voluminous enough without including our stuff in there as well (and it tends to be
not that helpful, anyway)
(2) pytest captures all output and shows it when the test ends i.e. we don't get to see messages in real-time.
"""
for logger in utils.get_all_loggers():
# NOTE: FileHandler derives from StreamHandler, and we want to keep those, so we can't use isinstance().
handlers = [ h for h in logger.handlers if type( h ) is logging.StreamHandler ] #pylint: disable=unidiomatic-typecheck
for h in handlers:
logger.removeHandler( h )