Skip to content

Commit

Permalink
use pytest in run_tests() (#1129)
Browse files Browse the repository at this point in the history
  • Loading branch information
keitherskine authored Nov 30, 2022
1 parent b8e8522 commit 23ca23a
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ x86/
pyodbc.conf
tmp
tags
xxx_*

# The Access unit tests copy empty.accdb and empty.mdb to these names and use them.
test.accdb
Expand Down
1 change: 1 addition & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest
146 changes: 117 additions & 29 deletions tests3/run_tests.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,109 @@
#!/usr/bin/python
import configparser
import os
import sys
from typing import List, Optional, Tuple

import testutils

import pyodbc
import pytest

def main(sqlserver=None, postgresql=None, mysql=None, verbose=0):

# there is an assumption here about where this file is located
def option_transform(optionstr: str) -> str:
# the default ConfigParser() behavior is to lowercase key values,
# override this by simply returning the original key value
return optionstr


def generate_connection_string(attrs: List[Tuple[str, str]]) -> str:
attrs_str_list = []
for key, value in attrs:
# escape/bookend values that include special characters
# ref: https://learn.microsoft.com/en-us/openspecs/sql_server_protocols/ms-odbcstr/348b0b4d-358a-41fb-9753-6351425809cb
if any(c in value for c in ';} '):
value = '{{{}}}'.format(value.replace('}', '}}'))

attrs_str_list.append(f'{key}={value}')

conn_str = ';'.join(attrs_str_list)
return conn_str


def read_db_config() -> Tuple[List[str], List[str], List[str]]:
sqlserver = []
postgresql = []
mysql = []

# get the filename of the database configuration file
pyodbc_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
default_cfg_file = os.path.join(pyodbc_dir, 'tmp', 'database.cfg')
cfg_file = os.getenv('PYODBC_DATABASE_CFG', default_cfg_file)

if os.path.exists(cfg_file):
print(f'Using database configuration file: {cfg_file}')

# read the contents of the config file
config = configparser.ConfigParser()
config.optionxform = option_transform # prevents keys from being lowercased
config.read(cfg_file)

# generate the connection strings
for section in config.sections():
section_lower = section.lower()
if section_lower.startswith('sqlserver'):
conn_string = generate_connection_string(config.items(section))
sqlserver.append(conn_string)
elif section_lower.startswith('postgres'):
conn_string = generate_connection_string(config.items(section))
postgresql.append(conn_string)
elif section_lower.startswith('mysql'):
conn_string = generate_connection_string(config.items(section))
mysql.append(conn_string)
else:
print(f'Database configuration file not found: {cfg_file}')

return sqlserver, postgresql, mysql


def main(sqlserver: Optional[List[str]] = None,
postgresql: Optional[List[str]] = None,
mysql: Optional[List[str]] = None,
verbose: int = 0,
quiet: int = 0,
k_expression: Optional[str] = None) -> bool:

# read from the config file if no connection strings provided
if not (sqlserver or postgresql or mysql):
sqlserver, postgresql, mysql = read_db_config()

if not (sqlserver or postgresql or mysql):
print('No tests have been run because no database connection info was provided')
return False

tests_dir = os.path.dirname(os.path.abspath(__file__))

databases = {
'SQL Server': {
'conn_strs': sqlserver or [],
'discovery_start_dir': os.path.join(pyodbc_dir, 'tests3'),
'discovery_pattern': 'sqlservertests.py',
'discovery_patterns': [
# FUTURE: point to dir specific to SQL Server - os.path.join(tests_dir, 'sqlserver'),
os.path.join(tests_dir, 'sqlservertests.py'),
],
},
'PostgreSQL': {
'conn_strs': postgresql or [],
'discovery_start_dir': os.path.join(pyodbc_dir, 'tests3'),
'discovery_pattern': 'pgtests.py',
'discovery_patterns': [
# FUTURE: point to dir specific to PostgreSQL - os.path.join(tests_dir, 'postgresql'),
os.path.join(tests_dir, 'pgtests.py'),
],
},
'MySQL': {
'conn_strs': mysql or [],
'discovery_start_dir': os.path.join(pyodbc_dir, 'tests3'),
'discovery_pattern': 'mysqltests.py',
'discovery_patterns': [
# FUTURE: point to dir specific to MySQL - os.path.join(tests_dir, 'mysql'),
os.path.join(tests_dir, 'mysqltests.py'),
],
},
}

Expand All @@ -39,18 +118,30 @@ def main(sqlserver=None, postgresql=None, mysql=None, verbose=0):
testutils.print_library_info(cnxn)
cnxn.close()

# it doesn't seem to be possible to pass test parameters into the test
# discovery process, so the connection string will have to be passed to
# the test cases via an environment variable
# it doesn't seem to be easy to pass test parameters into the test
# discovery process, so the connection string will have to be passed
# to the test cases via an environment variable
os.environ['PYODBC_CONN_STR'] = db_conn_str

result = testutils.discover_and_run(
top_level_dir=pyodbc_dir,
start_dir=db_attrs['discovery_start_dir'],
pattern=db_attrs['discovery_pattern'],
verbosity=verbose,
)
if not result.wasSuccessful():
# construct arguments for pytest
pytest_args = []

if verbose > 0:
pytest_args.extend(['-v'] * verbose)
elif quiet > 0:
pytest_args.extend(['-q'] * quiet)

if k_expression:
pytest_args.extend(['-k', k_expression])

pytest_args.extend(db_attrs['discovery_patterns'])

# run the tests
retcode = pytest.main(args=pytest_args)
if retcode == pytest.ExitCode.NO_TESTS_COLLECTED:
print('No tests collected during discovery')
overall_result = False
elif retcode != pytest.ExitCode.OK:
overall_result = False

return overall_result
Expand All @@ -59,19 +150,16 @@ def main(sqlserver=None, postgresql=None, mysql=None, verbose=0):
if __name__ == '__main__':
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument("--sqlserver", nargs='*', help="connection string(s) for SQL Server")
parser.add_argument("--postgresql", nargs='*', help="connection string(s) for PostgreSQL")
parser.add_argument("--mysql", nargs='*', help="connection string(s) for MySQL")
parser.add_argument("-v", "--verbose", action="count", default=0, help="increment test verbosity (can be used multiple times)")
parser.add_argument("--sqlserver", action="append", help="connection string for SQL Server")
parser.add_argument("--postgresql", action="append", help="connection string for PostgreSQL")
parser.add_argument("--mysql", action="append", help="connection string for MySQL")
parser.add_argument("-k", dest="k_expression", help="run tests whose names match the expression")
qv_group = parser.add_mutually_exclusive_group()
qv_group.add_argument("-q", "--quiet", action="count", default=0, help="decrease test verbosity (can be used multiple times)")
qv_group.add_argument("-v", "--verbose", action="count", default=0, help="increment test verbosity (can be used multiple times)")
# TODO: gather any remaining args and include in call to pytest??? i.e. known_args, other_args = parser.parse_known_args()
args = parser.parse_args()

# add the build directory to the Python path so we're testing the latest
# build, not the pip-installed version
testutils.add_to_path()

# only after setting the Python path, import the pyodbc module
import pyodbc

# run the tests
passed = main(**vars(args))
sys.exit(0 if passed else 1)
36 changes: 28 additions & 8 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,44 @@
# First, install tox. Tox should typically be available from the command line so it
# is recommended to install it using pipx (pipx install tox).
#
# Run tests against multiple databases by providing connection strings as parameters,
# for example:
# tox -- --sqlserver "DSN=localhost18" --postgresql "DSN=pg11" --mysql "DSN=mysql57"
# Run tests against SQL Server, Postgres, and MySQL databases by providing connection
# strings as parameters, for example:
# tox -- --sqlserver "DSN=localhost19" --postgresql "DSN=pg11" --mysql "DSN=mysql57"
# You can test against multiple versions of the same database, here with added verbosity:
# tox -- --sqlserver "DSN=localhost17" "DSN=localhost18" -v
# tox -- --sqlserver "DSN=localhost17" --sqlserver "DSN=localhost19" -v
# Run tests with specific names by using the -k option (per pytest), here in quiet mode:
# tox -- --sqlserver "DSN=localhost17" -k "unicode" -q
# Note the use of "--" to separate the "tox" parameters from the parameters for the
# tests. Also, the databases must be up and available before running the tests.
# Currently, only SQL Server, Postgres, and MySQL are supported through tox.
# tests.
#
# Alternatively, database connection info can be provided in a configuration file, in
# the standard INI format. The default filename is "tmp/database.cfg" within this repo,
# but this can be overriden with the PYODBC_DATABASE_CFG environment variable. Here
# is an example of a suitable configuration file:
# [sqlserver_2017]
# DSN=localhost17
#
# [sqlserver_2019]
# DSN=localhost19
#
# [POSTGRES]
# user = test
# password = test_password
# Any section names beginning with "sqlserver", "postgres" or "mysql" (case-insensitive)
# will be used for testing. The key/value pairs in each section will be used to connect
# to the relevant database.
#
# Naturally, test databases must be up and available before running the tests.
# Currently, only SQL Server, PostgreSQL, and MySQL are supported through tox.
# Python 2.7 is not supported.

[tox]
skipsdist = true

[testenv:unit_tests]
description = Run the pyodbc unit tests
deps = pytest
deps = -r requirements-test.txt
sitepackages = false
commands =
python setup.py build
python -m pip install --force-reinstall --no-deps .
python .{/}tests3{/}run_tests.py {posargs}

0 comments on commit 23ca23a

Please sign in to comment.