From 9159d35521635543a4c153edf1e06bb3378795ce Mon Sep 17 00:00:00 2001 From: Keith Erskine Date: Sun, 7 Aug 2022 19:47:04 -0500 Subject: [PATCH 1/5] update unit tests for tox --- tests3/mysqltests.py | 83 +++++++++++++++++++------------ tests3/pgtests.py | 105 ++++++++++++++++++++++++--------------- tests3/sqlservertests.py | 89 +++++++++++++++++++-------------- 3 files changed, 168 insertions(+), 109 deletions(-) diff --git a/tests3/mysqltests.py b/tests3/mysqltests.py index eaefc87c..18647734 100755 --- a/tests3/mysqltests.py +++ b/tests3/mysqltests.py @@ -22,7 +22,12 @@ from decimal import Decimal from datetime import datetime, date, time from os.path import join, getsize, dirname, abspath, basename -from testutils import * + +if __name__ != '__main__': + import pyodbc + +import testutils + _TESTSTR = '0123456789-abcdefghijklmnopqrstuvwxyz-' @@ -54,9 +59,12 @@ class MySqlTestCase(unittest.TestCase): STR_FENCEPOSTS = [ _generate_test_string(size) for size in SMALL_FENCEPOST_SIZES ] BLOB_FENCEPOSTS = STR_FENCEPOSTS + [ _generate_test_string(size) for size in LARGE_FENCEPOST_SIZES ] - def __init__(self, method_name, connection_string): + def __init__(self, method_name, connection_string=None): unittest.TestCase.__init__(self, method_name) - self.connection_string = connection_string + if connection_string is not None: + self.connection_string = connection_string + else: + self.connection_string = os.environ['PYODBC_CONN_STR'] def setUp(self): self.cnxn = pyodbc.connect(self.connection_string) @@ -747,46 +755,55 @@ def test_emoticons_as_literal(self): self.assertEqual(result, v) def main(): - from optparse import OptionParser - parser = OptionParser(usage=usage) - parser.add_option("-v", "--verbose", action="count", default=0, help="Increment test verbosity (can be used multiple times)") - parser.add_option("-d", "--debug", action="store_true", default=False, help="Print debugging items") - parser.add_option("-t", "--test", help="Run only the named test") - - (options, args) = parser.parse_args() - - if len(args) > 1: + from argparse import ArgumentParser + parser = ArgumentParser(usage=usage) + parser.add_argument("-v", "--verbose", action="count", default=0, help="increment test verbosity (can be used multiple times)") + parser.add_argument("-d", "--debug", action="store_true", default=False, help="print debugging items") + parser.add_argument("-t", "--test", help="run only the named test") + parser.add_argument("--mysql", nargs='*', help="connection string(s) for MySQL") + # typically, the connection string is provided as the only parameter, so handle this case + parser.add_argument('conn_str', nargs='*', help="connection string for MySQL") + args = parser.parse_args() + + if len(args.conn_str) > 1: parser.error('Only one argument is allowed. Do you need quotes around the connection string?') - if not args: - filename = basename(sys.argv[0]) - assert filename.endswith('.py') - connection_string = load_setup_connection_string(filename[:-3]) - - if not connection_string: - parser.print_help() - raise SystemExit() + if args.mysql is not None: + connection_strings = args.mysql + elif len(args.conn_str) == 1 and args.conn_str[0]: + connection_strings = [args.conn_str[0]] else: - connection_string = args[0] + config_conn_string = testutils.load_setup_connection_string('mysqltests') + if config_conn_string is None: + parser.print_help() + return True # no connection string, therefore nothing to do + else: + connection_strings = [config_conn_string] - if options.verbose: - cnxn = pyodbc.connect(connection_string) - print_library_info(cnxn) + if args.verbose: + cnxn = pyodbc.connect(connection_strings[0]) + testutils.print_library_info(cnxn) cnxn.close() - suite = load_tests(MySqlTestCase, options.test, connection_string) + overall_result = True + for connection_string in connection_strings: + print(f'Running tests with connection string: {connection_string}') + suite = testutils.load_tests(MySqlTestCase, args.test, connection_string) + testRunner = unittest.TextTestRunner(verbosity=args.verbose) + result = testRunner.run(suite) + if not result.wasSuccessful(): + overall_result = False - testRunner = unittest.TextTestRunner(verbosity=options.verbose) - result = testRunner.run(suite) - - return result + return overall_result if __name__ == '__main__': - # Add the build directory to the path so we're testing the latest build, not the installed version. - - add_to_path() + # add the build directory to the path so we're testing the latest build, not the installed version. + testutils.add_to_path() + # only after setting the path, import pyodbc import pyodbc - sys.exit(0 if main().wasSuccessful() else 1) + + # run the tests + sys.exit(0 if main() else 1) diff --git a/tests3/pgtests.py b/tests3/pgtests.py index 9943bc2b..25690de8 100755 --- a/tests3/pgtests.py +++ b/tests3/pgtests.py @@ -19,11 +19,17 @@ Note: Be sure to use the "Unicode" (not the "ANSI") version of the PostgreSQL ODBC driver. """ +import os import sys import uuid import unittest from decimal import Decimal -from testutils import * + +if __name__ != '__main__': + import pyodbc + +import testutils + _TESTSTR = '0123456789-abcdefghijklmnopqrstuvwxyz-' @@ -59,9 +65,12 @@ class PGTestCase(unittest.TestCase): SMALL_BYTES = bytes(SMALL_STRING, 'utf-8') LARGE_BYTES = bytes(LARGE_STRING, 'utf-8') - def __init__(self, connection_string, ansi, method_name): + def __init__(self, method_name, connection_string=None, ansi=False): unittest.TestCase.__init__(self, method_name) - self.connection_string = connection_string + if connection_string is not None: + self.connection_string = connection_string + else: + self.connection_string = os.environ['PYODBC_CONN_STR'] self.ansi = ansi def setUp(self): @@ -707,56 +716,72 @@ def convert(value): self.assertEqual(value, '123.45') def main(): - from optparse import OptionParser - parser = OptionParser(usage="usage: %prog [options] connection_string") - parser.add_option("-v", "--verbose", default=0, action="count", help="Increment test verbosity (can be used multiple times)") - parser.add_option("-d", "--debug", action="store_true", default=False, help="Print debugging items") - parser.add_option("-t", "--test", help="Run only the named test") - parser.add_option('-a', '--ansi', help='ANSI only', default=False, action='store_true') - - (options, args) = parser.parse_args() - - if len(args) > 1: + from argparse import ArgumentParser + parser = ArgumentParser(usage=usage) + parser.add_argument("-v", "--verbose", action="count", default=0, help="increment test verbosity (can be used multiple times)") + parser.add_argument("-d", "--debug", action="store_true", default=False, help="print debugging items") + parser.add_argument("-t", "--test", help="run only the named test") + parser.add_argument("-a", "--ansi", action="store_true", default=False, help="ANSI only") + parser.add_argument("--postgresql", nargs='*', help="connection string(s) for PostgreSQL") + # typically, the connection string is provided as the only parameter, so handle this case + parser.add_argument('conn_str', nargs='*', help="connection string for PostgreSQL") + args = parser.parse_args() + + if len(args.conn_str) > 1: parser.error('Only one argument is allowed. Do you need quotes around the connection string?') - if not args: - connection_string = load_setup_connection_string('pgtests') - - if not connection_string: - parser.print_help() - raise SystemExit() + if args.postgresql is not None: + connection_strings = args.postgresql + elif len(args.conn_str) == 1 and args.conn_str[0]: + connection_strings = [args.conn_str[0]] else: - connection_string = args[0] + config_conn_string = testutils.load_setup_connection_string('pgtests') + if config_conn_string is None: + parser.print_help() + return True # no connection string, therefore nothing to do + else: + connection_strings = [config_conn_string] - if options.verbose: - cnxn = pyodbc.connect(connection_string, ansi=options.ansi) - print_library_info(cnxn) + if args.verbose: + cnxn = pyodbc.connect(connection_strings[0], ansi=args.ansi) + testutils.print_library_info(cnxn) cnxn.close() - if options.test: - # Run a single test - if not options.test.startswith('test_'): - options.test = 'test_%s' % (options.test) + overall_result = True + for connection_string in connection_strings: + print(f'Running tests with connection string: {connection_string}') - s = unittest.TestSuite([ PGTestCase(connection_string, options.ansi, options.test) ]) - else: - # Run all tests in the class + if args.test: + # Run a single test + if not args.test.startswith('test_'): + args.test = 'test_%s' % (args.test) - methods = [ m for m in dir(PGTestCase) if m.startswith('test_') ] - methods.sort() - s = unittest.TestSuite([ PGTestCase(connection_string, options.ansi, m) for m in methods ]) + suite = unittest.TestSuite([ + PGTestCase(method_name=args.test, connection_string=connection_string, ansi=args.ansi) + ]) + else: + # Run all tests in the class + methods = [ m for m in dir(PGTestCase) if m.startswith('test_') ] + methods.sort() + suite = unittest.TestSuite([ + PGTestCase( method_name=m, connection_string=connection_string, ansi=args.ansi) for m in methods + ]) - testRunner = unittest.TextTestRunner(verbosity=options.verbose) - result = testRunner.run(s) + testRunner = unittest.TextTestRunner(verbosity=args.verbose) + result = testRunner.run(suite) + if not result.wasSuccessful(): + overall_result = False - return result + return overall_result if __name__ == '__main__': - # Add the build directory to the path so we're testing the latest build, not the installed version. - - add_to_path() + # add the build directory to the path so we're testing the latest build, not the installed version. + testutils.add_to_path() + # only after setting the path, import pyodbc import pyodbc - sys.exit(0 if main().wasSuccessful() else 1) + + # run the tests + sys.exit(0 if main() else 1) diff --git a/tests3/sqlservertests.py b/tests3/sqlservertests.py index 76a1bb7a..795b00e1 100755 --- a/tests3/sqlservertests.py +++ b/tests3/sqlservertests.py @@ -1,10 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -x = 1 # Getting an error if starting with usage for some reason. - usage = """\ -usage: %prog [options] connection_string +%(prog)s [options] connection_string Unit tests for SQL Server. To use, pass a connection string as the parameter. The tests will create and drop tables t1 and t2 as necessary. @@ -34,7 +32,12 @@ from datetime import datetime, date, time from os.path import join, getsize, dirname, abspath from warnings import warn -from testutils import * + +if __name__ != '__main__': + import pyodbc + +import testutils + # Some tests have fallback code for known driver issues. # Change this value to False to bypass the fallback code, e.g., to see @@ -72,9 +75,12 @@ class SqlServerTestCase(unittest.TestCase): BYTE_FENCEPOSTS = [ bytes(s, 'ascii') for s in STR_FENCEPOSTS ] IMAGE_FENCEPOSTS = BYTE_FENCEPOSTS + [ bytes(_generate_test_string(size), 'ascii') for size in LARGE_FENCEPOST_SIZES ] - def __init__(self, method_name, connection_string): + def __init__(self, method_name, connection_string=None): unittest.TestCase.__init__(self, method_name) - self.connection_string = connection_string + if connection_string is not None: + self.connection_string = connection_string + else: + self.connection_string = os.environ['PYODBC_CONN_STR'] def driver_type_is(self, type_name): recognized_types = { @@ -1932,44 +1938,55 @@ def test_tvp_diffschema(self): self._test_tvp(True) def main(): - from optparse import OptionParser - parser = OptionParser(usage=usage) - parser.add_option("-v", "--verbose", action="count", default=0, help="Increment test verbosity (can be used multiple times)") - parser.add_option("-d", "--debug", action="store_true", default=False, help="Print debugging items") - parser.add_option("-t", "--test", help="Run only the named test") - - (options, args) = parser.parse_args() - - if len(args) > 1: - parser.error('Only one argument is allowed. Do you need quotes around the connection string?') - - if not args: - connection_string = load_setup_connection_string('sqlservertests') - - if not connection_string: - parser.print_help() - raise SystemExit() + from argparse import ArgumentParser + parser = ArgumentParser(usage=usage) + parser.add_argument("-v", "--verbose", action="count", default=0, help="increment test verbosity (can be used multiple times)") + parser.add_argument("-d", "--debug", action="store_true", default=False, help="print debugging items") + parser.add_argument("-t", "--test", help="run only the named test") + parser.add_argument("--sqlserver", nargs='*', help="connection string(s) for SQL Server") + # typically, the connection string is provided as the only parameter, so handle this case + parser.add_argument('conn_str', nargs='*', help="connection string for SQL Server") + args = parser.parse_args() + + if len(args.conn_str) > 1: + parser.error('Only one positional argument is allowed. Do you need quotes around the connection string?') + + if args.sqlserver is not None: + connection_strings = args.sqlserver + elif len(args.conn_str) == 1 and args.conn_str[0]: + connection_strings = [args.conn_str[0]] else: - connection_string = args[0] + config_conn_string = testutils.load_setup_connection_string('sqlservertests') + if config_conn_string is None: + parser.print_help() + return True # no connection string, therefore nothing to do + else: + connection_strings = [config_conn_string] - if options.verbose: - cnxn = pyodbc.connect(connection_string) - print_library_info(cnxn) + if args.verbose: + cnxn = pyodbc.connect(connection_strings[0]) + testutils.print_library_info(cnxn) cnxn.close() - suite = load_tests(SqlServerTestCase, options.test, connection_string) - - testRunner = unittest.TextTestRunner(verbosity=options.verbose) - result = testRunner.run(suite) + overall_result = True + for connection_string in connection_strings: + print(f'Running tests with connection string: {connection_string}') + suite = testutils.load_tests(SqlServerTestCase, args.test, connection_string) + testRunner = unittest.TextTestRunner(verbosity=args.verbose) + result = testRunner.run(suite) + if not result.wasSuccessful(): + overall_result = False - return result + return overall_result if __name__ == '__main__': - # Add the build directory to the path so we're testing the latest build, not the installed version. - - add_to_path() + # add the build directory to the path so we're testing the latest build, not the installed version + testutils.add_to_path() + # only after setting the path, import pyodbc import pyodbc - sys.exit(0 if main().wasSuccessful() else 1) + + # run the tests + sys.exit(0 if main() else 1) From d5e2110c2f0475efe469f4c16902e072f0b73f05 Mon Sep 17 00:00:00 2001 From: Keith Erskine Date: Sun, 7 Aug 2022 20:05:17 -0500 Subject: [PATCH 2/5] add_to_path uses importlib.machinery --- tests3/testutils.py | 61 ++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/tests3/testutils.py b/tests3/testutils.py index 809fb8cb..b52fbf0c 100644 --- a/tests3/testutils.py +++ b/tests3/testutils.py @@ -1,40 +1,39 @@ -import os, sys, platform +from datetime import datetime +import importlib.machinery +import os from os.path import join, dirname, abspath +import platform +import sys import unittest -from distutils.util import get_platform + def add_to_path(): """ - Prepends the build directory to the path so that newly built pyodbc libraries are used, allowing it to be tested - without installing it. + Prepends the build directory to the path so that newly built pyodbc libraries are + used, allowing it to be tested without installing it. """ - # Put the build directory into the Python path so we pick up the version we just built. - # - # To make this cross platform, we'll search the directories until we find the .pyd file. - - import imp - - library_exts = [ t[0] for t in imp.get_suffixes() if t[-1] == imp.C_EXTENSION ] - library_names = [ 'pyodbc%s' % ext for ext in library_exts ] - - # Only go into directories that match our version number. - - dir_suffix = '%s-%s.%s' % (get_platform(), sys.version_info[0], sys.version_info[1]) - - build = join(dirname(dirname(abspath(__file__))), 'build') - - for root, dirs, files in os.walk(build): - for d in dirs[:]: - if not d.endswith(dir_suffix): - dirs.remove(d) - - for name in library_names: - if name in files: - sys.path.insert(0, root) - print('Library:', join(root, name)) - return - - print('Did not find the pyodbc library in the build directory. Will use an installed version.') + # look for the suffixes used in the build filenames, e.g. ".cp38-win_amd64.pyd", ".cpython-38-darwin.so", etc. + library_exts = [ext for ext in importlib.machinery.EXTENSION_SUFFIXES if ext != '.pyd'] + # generate the name of the pyodbc build file(s) + library_names = ['pyodbc%s' % ext for ext in library_exts] + + build_dir = join(dirname(dirname(abspath(__file__))), 'build') + + # find all the relevant pyodbc build files, including modified date + file_info = [ + (os.path.getmtime(join(dirpath, file)), join(dirpath, file)) + for dirpath, dirs, files in os.walk(build_dir) + for file in files + if file in library_names + ] + if file_info: + file_info.sort() # put them in chronological order + library_modified_dt, library_path = file_info[-1] # use the latest one + # add the build directory to the Python path + sys.path.insert(0, dirname(library_path)) + print('Library: {} (last modified {})'.format(library_path, datetime.fromtimestamp(library_modified_dt))) + else: + print('Did not find the pyodbc library in the build directory. Will use the installed version.') def print_library_info(cnxn): From 97ad89718aa545cb0f1f6d8dceacd9e602b8f67b Mon Sep 17 00:00:00 2001 From: Keith Erskine Date: Sun, 7 Aug 2022 20:32:10 -0500 Subject: [PATCH 3/5] add tox --- tests3/__init__.py | 0 tests3/run_tests.py | 73 +++++++++++++++++++++++++++++++++++++++++++++ tests3/testutils.py | 10 ++++++- tox.ini | 29 ++++++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 tests3/__init__.py create mode 100644 tests3/run_tests.py create mode 100644 tox.ini diff --git a/tests3/__init__.py b/tests3/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests3/run_tests.py b/tests3/run_tests.py new file mode 100644 index 00000000..81cd0028 --- /dev/null +++ b/tests3/run_tests.py @@ -0,0 +1,73 @@ +#!/usr/bin/python +import os + +import testutils + + +def main(sqlserver=None, postgresql=None, mysql=None, verbose=0): + + # there is an assumption here about where this file is located + pyodbc_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + # TODO: move the test scripts into separate folders for each database so that + # multiple test scripts for each database can easily be discovered + databases = { + 'SQL Server': { + 'conn_strs': sqlserver or [], + 'discovery_start_dir': os.path.join(pyodbc_dir, 'tests3'), + 'discovery_pattern': 'sqlservertests.py', + }, + 'PostgreSQL': { + 'conn_strs': postgresql or [], + 'discovery_start_dir': os.path.join(pyodbc_dir, 'tests3'), + 'discovery_pattern': 'pgtests.py', + }, + 'MySQL': { + 'conn_strs': mysql or [], + 'discovery_start_dir': os.path.join(pyodbc_dir, 'tests3'), + 'discovery_pattern': 'mysqltests.py', + }, + } + + overall_result = True + for db_name, db_attrs in databases.items(): + + for db_conn_str in db_attrs['conn_strs']: + + print(f'Running tests against {db_name} with connection string: {db_conn_str}') + os.environ['PYODBC_CONN_STR'] = db_conn_str + + if verbose > 0: + cnxn = pyodbc.connect(db_conn_str) + testutils.print_library_info(cnxn) + cnxn.close() + + 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(): + overall_result = False + + return overall_result + + +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)") + args = parser.parse_args() + + # add the build directory to the path so we're testing the latest build, not the installed version + testutils.add_to_path() + + # only after setting the path, import pyodbc + import pyodbc + + # run the tests + main(**vars(args)) diff --git a/tests3/testutils.py b/tests3/testutils.py index b52fbf0c..7d9f5463 100644 --- a/tests3/testutils.py +++ b/tests3/testutils.py @@ -12,11 +12,13 @@ def add_to_path(): Prepends the build directory to the path so that newly built pyodbc libraries are used, allowing it to be tested without installing it. """ - # look for the suffixes used in the build filenames, e.g. ".cp38-win_amd64.pyd", ".cpython-38-darwin.so", etc. + # look for the suffixes used in the build filenames, e.g. ".cp38-win_amd64.pyd", + # ".cpython-38-darwin.so", ".cpython-38-x86_64-linux-gnu.so", etc. library_exts = [ext for ext in importlib.machinery.EXTENSION_SUFFIXES if ext != '.pyd'] # generate the name of the pyodbc build file(s) library_names = ['pyodbc%s' % ext for ext in library_exts] + # there is an assumption here about where this file is located build_dir = join(dirname(dirname(abspath(__file__))), 'build') # find all the relevant pyodbc build files, including modified date @@ -57,6 +59,12 @@ def print_library_info(cnxn): print(' %s' % ' '.join([s for s in platform.win32_ver() if s])) +def discover_and_run(top_level_dir='.', start_dir='.', pattern='test*.py', verbosity=0): + tests = unittest.defaultTestLoader.discover(top_level_dir=top_level_dir, start_dir=start_dir, pattern=pattern) + runner = unittest.TextTestRunner(verbosity=verbosity) + result = runner.run(tests) + return result + def load_tests(testclass, name, *args): """ diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..9b3836d3 --- /dev/null +++ b/tox.ini @@ -0,0 +1,29 @@ +# RUNNING UNIT TESTS +# Install tox (pip install tox). Typically tox should be installed globally rather than +# in a virtual environment, alternatively use pipx. It's also recommended to install +# tox in the oldest Python version you will be testing with (see "envlist" below) and +# running tox from there. Running tox against later versions of Python is generally +# OK, but running tox against an earlier Python version might not be. +# +# Run tests against multiple databases (which must already be up and available) by +# providing connection strings as parameters. Note the use of "--" to separate the +# parameters for tox from the parameters for the tests: +# tox -- --sqlserver "DSN=localhost18" --postgresql "DSN=pg54" --mysql "DSN=msql81" +# Run tests against multiple versions of the same database, here with added verbosity: +# tox -- --sqlserver "DSN=localhost17" "DSN=localhost18" -v + +[tox] +# for convenience and speed, test against only one version of Python, the oldest +# version (Python3) supported by pyodbc +envlist = py36 +# to run against multiple versions of Python, use the following instead: +# envlist = py{36,37,38,39,310} +skipsdist = true + +[testenv] +description = Test pyodbc +deps = pytest +sitepackages = false +commands = + python setup.py build + python .{/}tests3{/}run_tests.py {posargs} From 737e71f1e022b5ca381c6cac994b68f0a7a4c065 Mon Sep 17 00:00:00 2001 From: Keith Erskine Date: Sat, 5 Nov 2022 09:54:29 -0500 Subject: [PATCH 4/5] multiple tweaks --- src/pyodbc.pyi | 3 +++ tests3/mysqltests.py | 7 +++++-- tests3/pgtests.py | 7 +++++-- tests3/run_tests.py | 14 ++++++++------ tests3/sqlservertests.py | 7 +++++-- tests3/testutils.py | 7 ++++--- tox.ini | 32 ++++++++++++++------------------ 7 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/pyodbc.pyi b/src/pyodbc.pyi index 98a17d6a..ec818a6c 100644 --- a/src/pyodbc.pyi +++ b/src/pyodbc.pyi @@ -293,6 +293,9 @@ SQL_XOPEN_CLI_YEAR: int # pyodbc-specific constants BinaryNull: Any # to distinguish binary NULL values from char NULL values +UNICODE_SIZE: int +SQLWCHAR_SIZE: int + # module attributes # https://www.python.org/dev/peps/pep-0249/#globals diff --git a/tests3/mysqltests.py b/tests3/mysqltests.py index 30738d72..131d68f1 100644 --- a/tests3/mysqltests.py +++ b/tests3/mysqltests.py @@ -64,6 +64,8 @@ def __init__(self, method_name, connection_string=None): if connection_string is not None: self.connection_string = connection_string else: + # if the connection string cannot be provided directly here, it can be + # provided in an environment variable self.connection_string = os.environ['PYODBC_CONN_STR'] def setUp(self): @@ -799,10 +801,11 @@ def main(): if __name__ == '__main__': - # add the build directory to the path so we're testing the latest build, not the installed version. + # 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 path, import pyodbc + # only after setting the Python path, import the pyodbc module import pyodbc # run the tests diff --git a/tests3/pgtests.py b/tests3/pgtests.py index ce7c5374..8724f884 100644 --- a/tests3/pgtests.py +++ b/tests3/pgtests.py @@ -70,6 +70,8 @@ def __init__(self, method_name, connection_string=None, ansi=False): if connection_string is not None: self.connection_string = connection_string else: + # if the connection string cannot be provided directly here, it can be + # provided in an environment variable self.connection_string = os.environ['PYODBC_CONN_STR'] self.ansi = ansi @@ -777,10 +779,11 @@ def main(): if __name__ == '__main__': - # add the build directory to the path so we're testing the latest build, not the installed version. + # 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 path, import pyodbc + # only after setting the Python path, import the pyodbc module import pyodbc # run the tests diff --git a/tests3/run_tests.py b/tests3/run_tests.py index 81cd0028..28d0eef7 100644 --- a/tests3/run_tests.py +++ b/tests3/run_tests.py @@ -9,8 +9,6 @@ def main(sqlserver=None, postgresql=None, mysql=None, verbose=0): # there is an assumption here about where this file is located pyodbc_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - # TODO: move the test scripts into separate folders for each database so that - # multiple test scripts for each database can easily be discovered databases = { 'SQL Server': { 'conn_strs': sqlserver or [], @@ -33,15 +31,18 @@ def main(sqlserver=None, postgresql=None, mysql=None, verbose=0): for db_name, db_attrs in databases.items(): for db_conn_str in db_attrs['conn_strs']: - print(f'Running tests against {db_name} with connection string: {db_conn_str}') - os.environ['PYODBC_CONN_STR'] = db_conn_str if verbose > 0: cnxn = pyodbc.connect(db_conn_str) 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 + 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'], @@ -63,10 +64,11 @@ def main(sqlserver=None, postgresql=None, mysql=None, verbose=0): parser.add_argument("-v", "--verbose", action="count", default=0, help="increment test verbosity (can be used multiple times)") args = parser.parse_args() - # add the build directory to the path so we're testing the latest build, not the installed version + # 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 path, import pyodbc + # only after setting the Python path, import the pyodbc module import pyodbc # run the tests diff --git a/tests3/sqlservertests.py b/tests3/sqlservertests.py index 795b00e1..8c6655f6 100644 --- a/tests3/sqlservertests.py +++ b/tests3/sqlservertests.py @@ -80,6 +80,8 @@ def __init__(self, method_name, connection_string=None): if connection_string is not None: self.connection_string = connection_string else: + # if the connection string cannot be provided directly here, it can be + # provided in an environment variable self.connection_string = os.environ['PYODBC_CONN_STR'] def driver_type_is(self, type_name): @@ -1982,10 +1984,11 @@ def main(): if __name__ == '__main__': - # add the build directory to the path so we're testing the latest build, not the installed version + # 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 path, import pyodbc + # only after setting the Python path, import the pyodbc module import pyodbc # run the tests diff --git a/tests3/testutils.py b/tests3/testutils.py index 7d9f5463..0f946340 100644 --- a/tests3/testutils.py +++ b/tests3/testutils.py @@ -10,7 +10,7 @@ def add_to_path(): """ Prepends the build directory to the path so that newly built pyodbc libraries are - used, allowing it to be tested without installing it. + used, allowing it to be tested without pip-installing it. """ # look for the suffixes used in the build filenames, e.g. ".cp38-win_amd64.pyd", # ".cpython-38-darwin.so", ".cpython-38-x86_64-linux-gnu.so", etc. @@ -18,10 +18,10 @@ def add_to_path(): # generate the name of the pyodbc build file(s) library_names = ['pyodbc%s' % ext for ext in library_exts] - # there is an assumption here about where this file is located + # the build directory is assumed to be one directory up from this file build_dir = join(dirname(dirname(abspath(__file__))), 'build') - # find all the relevant pyodbc build files, including modified date + # find all the relevant pyodbc build files, and get their modified dates file_info = [ (os.path.getmtime(join(dirpath, file)), join(dirpath, file)) for dirpath, dirs, files in os.walk(build_dir) @@ -60,6 +60,7 @@ def print_library_info(cnxn): def discover_and_run(top_level_dir='.', start_dir='.', pattern='test*.py', verbosity=0): + """Finds all the test cases in the start directory and runs them""" tests = unittest.defaultTestLoader.discover(top_level_dir=top_level_dir, start_dir=start_dir, pattern=pattern) runner = unittest.TextTestRunner(verbosity=verbosity) result = runner.run(tests) diff --git a/tox.ini b/tox.ini index 9b3836d3..c3fffeb4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +1,23 @@ -# RUNNING UNIT TESTS -# Install tox (pip install tox). Typically tox should be installed globally rather than -# in a virtual environment, alternatively use pipx. It's also recommended to install -# tox in the oldest Python version you will be testing with (see "envlist" below) and -# running tox from there. Running tox against later versions of Python is generally -# OK, but running tox against an earlier Python version might not be. +# USAGE +# 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 (which must already be up and available) by -# providing connection strings as parameters. Note the use of "--" to separate the -# parameters for tox from the parameters for the tests: -# tox -- --sqlserver "DSN=localhost18" --postgresql "DSN=pg54" --mysql "DSN=msql81" -# Run tests against multiple versions of the same database, here with added verbosity: +# Run tests against multiple databases by providing connection strings as parameters, +# for example: +# tox -- --sqlserver "DSN=localhost18" --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 +# 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. +# +# Python 2.7 is not supported. [tox] -# for convenience and speed, test against only one version of Python, the oldest -# version (Python3) supported by pyodbc -envlist = py36 -# to run against multiple versions of Python, use the following instead: -# envlist = py{36,37,38,39,310} skipsdist = true -[testenv] -description = Test pyodbc +[testenv:unit_tests] +description = Run the pyodbc unit tests deps = pytest sitepackages = false commands = From 5a5563c3a42a4bb8260180b296f52b541d1c6e01 Mon Sep 17 00:00:00 2001 From: Keith Erskine Date: Sun, 6 Nov 2022 10:26:45 -0600 Subject: [PATCH 5/5] add run_tests exit code --- tests3/run_tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests3/run_tests.py b/tests3/run_tests.py index 28d0eef7..e2ca8944 100644 --- a/tests3/run_tests.py +++ b/tests3/run_tests.py @@ -1,5 +1,6 @@ #!/usr/bin/python import os +import sys import testutils @@ -72,4 +73,5 @@ def main(sqlserver=None, postgresql=None, mysql=None, verbose=0): import pyodbc # run the tests - main(**vars(args)) + passed = main(**vars(args)) + sys.exit(0 if passed else 1)