Manage ASL magazines and their articles.
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.
 
 
 
 
 
asl-articles/conftest.py

250 lines
9.4 KiB

""" pytest support functions. """
import os
import threading
import tempfile
import urllib.request
import urllib.parse
from urllib.error import URLError
import pytest
import flask
import werkzeug
import sqlalchemy
from flask_sqlalchemy import SQLAlchemy
import alembic
import alembic.config
import asl_articles
from asl_articles import app
from asl_articles.utils import to_bool
from asl_articles import tests as asl_articles_tests
_FLASK_SERVER_URL = ( "localhost", 5001 ) # nb: for the test Flask server we spin up
_pytest_options = None
# ---------------------------------------------------------------------
def pytest_addoption( parser ):
"""Configure pytest options."""
# NOTE: This file needs to be in the project root for this to work :-/
# https://docs.pytest.org/en/latest/reference.html#initialization-hooks
# add test options
parser.addoption(
# NOTE: We assume that the React frontend server is already running.
"--web-url", action="store", dest="web_url", default="http://localhost:3000",
help="React server to test against."
)
parser.addoption(
"--flask-url", action="store", dest="flask_url", default=None,
help="Flask server to test against."
)
parser.addoption(
"--webdriver", action="store", dest="webdriver", default="chrome",
help="Webdriver to use (chrome|firefox)."
)
# add test options
parser.addoption(
"--headless", action="store_true", dest="headless", default=False,
help="Run the tests headless."
)
parser.addoption(
"--window", action="store", dest="window_size", default="1000x700",
help="Browser window size."
)
# add test options
parser.addoption(
"--dbconn", action="store", dest="dbconn", default=None,
help="Database connection string."
)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def pytest_configure( config ):
"""Called after command-line options have been parsed."""
global _pytest_options
_pytest_options = config.option
# notify the test suite about the pytest options
asl_articles_tests.pytest_options = _pytest_options
# ---------------------------------------------------------------------
@pytest.fixture( scope="session" )
def flask_app( request ):
"""Prepare the Flask server."""
# initialize
flask_url = request.config.getoption( "--flask-url" ) #pylint: disable=no-member
# initialize
# WTF?! https://github.com/pallets/flask/issues/824
def make_flask_url( endpoint, **kwargs ):
"""Generate a URL for the Flask backend server."""
with app.test_request_context():
url = flask.url_for( endpoint, _external=True, **kwargs )
if flask_url:
url = url.replace( "http://localhost", flask_url )
else:
url = url.replace( "localhost/", "{}:{}/".format(*_FLASK_SERVER_URL) )
return url
app.url_for = make_flask_url
# check if we need to start a local Flask server
if not flask_url:
# FUDGE! If we're going to create our own Flask server, we want to stop it from checking
# the *configured* database connection string (since it will fail to start if there's a problem).
asl_articles._disable_db_startup = True #pylint: disable=protected-access
# yup - make it so
server = werkzeug.serving.make_server(
_FLASK_SERVER_URL[0], _FLASK_SERVER_URL[1],
app, threaded=True
)
thread = threading.Thread(
target = server.serve_forever,
daemon=True
)
thread.start()
# wait for the server to start up
def is_ready():
"""Try to connect to the Flask server."""
try:
url = app.url_for( "ping" )
with urllib.request.urlopen( url ) as resp:
assert resp.read() == b"pong"
return True
except URLError:
return False
except Exception as ex: #pylint: disable=broad-except
assert False, "Unexpected exception: {}".format( ex )
asl_articles_tests.utils.wait_for( 5, is_ready )
# return the server to the caller
try:
yield app
finally:
# shutdown the local Flask server
if not flask_url:
server.shutdown()
thread.join()
# ---------------------------------------------------------------------
@pytest.fixture( scope="session" )
def webdriver( request ):
"""Prepare a webdriver that can be used to control a browser."""
# initialize
driver = request.config.getoption( "--webdriver" )
headless = request.config.getoption( "--headless" )
from selenium import webdriver as wb #pylint: disable=import-outside-toplevel
if driver == "firefox":
options = wb.FirefoxOptions()
if headless:
options.add_argument( "--headless" ) #pylint: disable=no-member
driver = wb.Firefox( options=options )
elif driver == "chrome":
options = wb.ChromeOptions()
if headless:
options.add_argument( "--headless" ) #pylint: disable=no-member
options.add_argument( "--disable-gpu" )
driver = wb.Chrome( options=options )
else:
raise RuntimeError( "Unknown webdriver: {}".format( driver ) )
# set the browser size
window_size = request.config.getoption( "--window" )
if window_size:
words = window_size.split( "x" ) #pylint: disable=no-member
driver.set_window_size( int(words[0]), int(words[1]) )
# figure out which Flask backend server the React frontend should talk to
flask_url = request.config.getoption( "--flask-url" )
if not flask_url:
# we're talking to our own test Flask server
flask_url = "http://{}:{}".format( *_FLASK_SERVER_URL )
# initialize
web_url = request.config.getoption( "--web-url" )
assert web_url
def make_web_url( url, **kwargs ):
"""Generate a URL for the React frontend."""
url = "{}/{}".format( web_url, url )
kwargs[ "_flask"] = flask_url
kwargs[ "store_msgs"] = 1 # stop notification messages from building up and obscuring clicks
kwargs[ "fake_uploads"] = 1 # alternate mechanism for uploading files
url += "&" if "?" in url else "?"
url += urllib.parse.urlencode( kwargs )
return url
driver.make_url = make_web_url
# return the webdriver to the caller
try:
yield driver
finally:
driver.quit()
# ---------------------------------------------------------------------
@pytest.fixture( scope="function" )
def dbconn( request ):
"""Prepare a database connection."""
# initialize
conn_string = request.config.getoption( "--dbconn" )
temp_fname = None
if conn_string:
if os.path.isfile( conn_string ):
# a file was specified - we assume it's an SQLite database
conn_string = "sqlite:///{}".format( conn_string )
else:
# create a temp file and install our database schema into it
with tempfile.NamedTemporaryFile( delete=False ) as temp_file:
temp_fname = temp_file.name
dname = os.path.join( os.path.split(__file__)[0], "alembic/" )
cfg = alembic.config.Config( os.path.join( dname, "alembic.ini" ) )
cfg.set_main_option( "script_location", dname )
conn_string = "sqlite:///{}".format( temp_fname )
cfg.set_main_option( "sqlalchemy.url", conn_string )
alembic.command.upgrade( cfg, "head" )
# connect to the database
engine = sqlalchemy.create_engine( conn_string,
echo = to_bool( app.config.get( "SQLALCHEMY_ECHO" ) )
)
# IMPORTANT! The test suite often loads the database with test data, and then runs searches to see what happens.
# In the normal case, this works fine:
# - we use either an existing database, or create a temp file as an sqlite database (see above)
# - this database is then installed into the temp Flask server that the "flask_app" fixture spun up, thus ensuring
# that both the backend server and test code are working with the same database.
# However, this doesn't work when the test suite is talking to a remote Flask server (via the --flask-url argument),
# since it has no way of ensuring that the remote Flask server is talking to the same database. In this case,
# it's the developer's responsibility to make sure that this is the case (by configuring the database in site.cfg).
prev_db_state = None
try:
flask_url = request.config.getoption( "--flask-url" ) #pylint: disable=no-member
if flask_url:
# we are talking to a remote Flask server, we assume it's already talking to the database we are
pass
else:
# remember the database our temp Flask server is currently using
prev_db_state = ( app.config["SQLALCHEMY_DATABASE_URI"], asl_articles.db )
# replace the database the temp Flask server with ours
app.config[ "SQLALCHEMY_DATABASE_URI" ] = conn_string
app.db = SQLAlchemy( app )
# return the database connection to the caller
yield engine
finally:
# restore the original database into our temp Flask server
if prev_db_state:
app.config[ "SQLALCHEMY_DATABASE_URI" ] = prev_db_state[0]
asl_articles.db = prev_db_state[1]
# clean up
if temp_fname:
os.unlink( temp_fname )