diff options
| author | mike bayer <mike_mp@zzzcomputing.com> | 2022-01-25 14:59:43 +0000 |
|---|---|---|
| committer | Gerrit Code Review <gerrit@ci3.zzzcomputing.com> | 2022-01-25 14:59:43 +0000 |
| commit | 0a298e70518b6dfd47ba0190d9c4abf0f0d3f4ba (patch) | |
| tree | 626062f700f78335304166f6cd88e044cef0a71b | |
| parent | 5e3357c70e419c244156ac3885b2cf784b5b3fc0 (diff) | |
| parent | ca48f461b2dcac2970829e4e021316654c308d90 (diff) | |
| download | sqlalchemy-0a298e70518b6dfd47ba0190d9c4abf0f0d3f4ba.tar.gz | |
Merge "replace test tags with pytest.mark" into main
| -rw-r--r-- | lib/sqlalchemy/testing/__init__.py | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/config.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/exclusions.py | 23 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/plugin/plugin_base.py | 123 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/plugin/pytestplugin.py | 52 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/requirements.py | 5 | ||||
| -rw-r--r-- | pyproject.toml | 10 | ||||
| -rw-r--r-- | test/aaa_profiling/test_memusage.py | 7 | ||||
| -rw-r--r-- | test/ext/mypy/test_mypy_plugin_py3k.py | 1 | ||||
| -rw-r--r-- | tox.ini | 16 |
10 files changed, 123 insertions, 123 deletions
diff --git a/lib/sqlalchemy/testing/__init__.py b/lib/sqlalchemy/testing/__init__.py index fd6ddf593..4253aa61b 100644 --- a/lib/sqlalchemy/testing/__init__.py +++ b/lib/sqlalchemy/testing/__init__.py @@ -42,6 +42,7 @@ from .assertions import not_in from .assertions import not_in_ from .assertions import startswith_ from .assertions import uses_deprecated +from .config import add_to_marker from .config import async_test from .config import combinations from .config import combinations_list diff --git a/lib/sqlalchemy/testing/config.py b/lib/sqlalchemy/testing/config.py index f326c124d..268a56421 100644 --- a/lib/sqlalchemy/testing/config.py +++ b/lib/sqlalchemy/testing/config.py @@ -106,6 +106,14 @@ def mark_base_test_class(): return _fixture_functions.mark_base_test_class() +class _AddToMarker: + def __getattr__(self, attr): + return getattr(_fixture_functions.add_to_marker, attr) + + +add_to_marker = _AddToMarker() + + class Config: def __init__(self, db, db_opts, options, file_config): self._set_name(db) diff --git a/lib/sqlalchemy/testing/exclusions.py b/lib/sqlalchemy/testing/exclusions.py index b92d6859f..b51f6e57c 100644 --- a/lib/sqlalchemy/testing/exclusions.py +++ b/lib/sqlalchemy/testing/exclusions.py @@ -35,7 +35,6 @@ class compound: def __init__(self): self.fails = set() self.skips = set() - self.tags = set() def __add__(self, other): return self.add(other) @@ -44,25 +43,22 @@ class compound: rule = compound() rule.skips.update(self.skips) rule.skips.update(self.fails) - rule.tags.update(self.tags) return rule def add(self, *others): copy = compound() copy.fails.update(self.fails) copy.skips.update(self.skips) - copy.tags.update(self.tags) + for other in others: copy.fails.update(other.fails) copy.skips.update(other.skips) - copy.tags.update(other.tags) return copy def not_(self): copy = compound() copy.fails.update(NotPredicate(fail) for fail in self.fails) copy.skips.update(NotPredicate(skip) for skip in self.skips) - copy.tags.update(self.tags) return copy @property @@ -83,16 +79,9 @@ class compound: if predicate(config) ] - def include_test(self, include_tags, exclude_tags): - return bool( - not self.tags.intersection(exclude_tags) - and (not include_tags or self.tags.intersection(include_tags)) - ) - def _extend(self, other): self.skips.update(other.skips) self.fails.update(other.fails) - self.tags.update(other.tags) def __call__(self, fn): if hasattr(fn, "_sa_exclusion_extend"): @@ -166,16 +155,6 @@ class compound: ) -def requires_tag(tagname): - return tags([tagname]) - - -def tags(tagnames): - comp = compound() - comp.tags.update(tagnames) - return comp - - def only_if(predicate, reason=None): predicate = _as_predicate(predicate) return skip_if(NotPredicate(predicate), reason) diff --git a/lib/sqlalchemy/testing/plugin/plugin_base.py b/lib/sqlalchemy/testing/plugin/plugin_base.py index 5a4bfe3a6..0b4451b3c 100644 --- a/lib/sqlalchemy/testing/plugin/plugin_base.py +++ b/lib/sqlalchemy/testing/plugin/plugin_base.py @@ -106,21 +106,28 @@ def setup_options(make_option): ) make_option( "--backend-only", - action="store_true", - dest="backend_only", - help="Run only tests marked with __backend__ or __sparse_backend__", + action="callback", + zeroarg_callback=_set_tag_include("backend"), + help=( + "Run only tests marked with __backend__ or __sparse_backend__; " + "this is now equivalent to the pytest -m backend mark expression" + ), ) make_option( "--nomemory", - action="store_true", - dest="nomemory", - help="Don't run memory profiling tests", + action="callback", + zeroarg_callback=_set_tag_exclude("memory_intensive"), + help="Don't run memory profiling tests; " + "this is now equivalent to the pytest -m 'not memory_intensive' " + "mark expression", ) make_option( "--notimingintensive", - action="store_true", - dest="notimingintensive", - help="Don't run timing intensive tests", + action="callback", + zeroarg_callback=_set_tag_exclude("timing_intensive"), + help="Don't run timing intensive tests; " + "this is now equivalent to the pytest -m 'not timing_intensive' " + "mark expression", ) make_option( "--profile-sort", @@ -171,26 +178,20 @@ def setup_options(make_option): help="requirements class for testing, overrides setup.cfg", ) make_option( - "--with-cdecimal", - action="store_true", - dest="cdecimal", - default=False, - help="Monkeypatch the cdecimal library into Python 'decimal' " - "for all tests", - ) - make_option( "--include-tag", action="callback", callback=_include_tag, type=str, - help="Include tests with tag <tag>", + help="Include tests with tag <tag>; " + "legacy, use pytest -m 'tag' instead", ) make_option( "--exclude-tag", action="callback", callback=_exclude_tag, type=str, - help="Exclude tests with tag <tag>", + help="Exclude tests with tag <tag>; " + "legacy, use pytest -m 'not tag' instead", ) make_option( "--write-profiles", @@ -240,15 +241,9 @@ def memoize_important_follower_config(dict_): This invokes in the parent process after normal config is set up. - This is necessary as pytest seems to not be using forking, so we - start with nothing in memory, *but* it isn't running our argparse - callables, so we have to just copy all of that over. + Hook is currently not used. """ - dict_["memoized_config"] = { - "include_tags": include_tags, - "exclude_tags": exclude_tags, - } def restore_important_follower_config(dict_): @@ -256,10 +251,9 @@ def restore_important_follower_config(dict_): This invokes in the follower process. + Hook is currently not used. + """ - global include_tags, exclude_tags - include_tags.update(dict_["memoized_config"]["include_tags"]) - exclude_tags.update(dict_["memoized_config"]["exclude_tags"]) def read_config(): @@ -322,6 +316,20 @@ def _requirements_opt(opt_str, value, parser): _setup_requirements(value) +def _set_tag_include(tag): + def _do_include_tag(opt_str, value, parser): + _include_tag(opt_str, tag, parser) + + return _do_include_tag + + +def _set_tag_exclude(tag): + def _do_exclude_tag(opt_str, value, parser): + _exclude_tag(opt_str, tag, parser) + + return _do_exclude_tag + + def _exclude_tag(opt_str, value, parser): exclude_tags.add(value.replace("-", "_")) @@ -350,26 +358,6 @@ def _setup_options(opt, file_config): options = opt -@pre -def _set_nomemory(opt, file_config): - if opt.nomemory: - exclude_tags.add("memory_intensive") - - -@pre -def _set_notimingintensive(opt, file_config): - if opt.notimingintensive: - exclude_tags.add("timing_intensive") - - -@pre -def _monkeypatch_cdecimal(options, file_config): - if options.cdecimal: - import cdecimal - - sys.modules["decimal"] = cdecimal - - @post def __ensure_cext(opt, file_config): if os.environ.get("REQUIRE_SQLALCHEMY_CEXT", "0") == "1": @@ -515,13 +503,6 @@ def want_class(name, cls): return False elif name.startswith("_"): return False - elif ( - config.options.backend_only - and not getattr(cls, "__backend__", False) - and not getattr(cls, "__sparse_backend__", False) - and not getattr(cls, "__only_on__", False) - ): - return False else: return True @@ -531,33 +512,13 @@ def want_method(cls, fn): return False elif fn.__module__ is None: return False - elif include_tags: - return ( - hasattr(cls, "__tags__") - and exclusions.tags(cls.__tags__).include_test( - include_tags, exclude_tags - ) - ) or ( - hasattr(fn, "_sa_exclusion_extend") - and fn._sa_exclusion_extend.include_test( - include_tags, exclude_tags - ) - ) - elif exclude_tags and hasattr(cls, "__tags__"): - return exclusions.tags(cls.__tags__).include_test( - include_tags, exclude_tags - ) - elif exclude_tags and hasattr(fn, "_sa_exclusion_extend"): - return fn._sa_exclusion_extend.include_test(include_tags, exclude_tags) else: return True -def generate_sub_tests(cls, module): - if getattr(cls, "__backend__", False) or getattr( - cls, "__sparse_backend__", False - ): - sparse = getattr(cls, "__sparse_backend__", False) +def generate_sub_tests(cls, module, markers): + if "backend" in markers or "sparse_backend" in markers: + sparse = "sparse_backend" in markers for cfg in _possible_configs_for_cls(cls, sparse=sparse): orig_name = cls.__name__ @@ -780,6 +741,10 @@ class FixtureFunctions(abc.ABC): def mark_base_test_class(self): raise NotImplementedError() + @abc.abstractproperty + def add_to_marker(self): + raise NotImplementedError() + _fixture_fn_class = None diff --git a/lib/sqlalchemy/testing/plugin/pytestplugin.py b/lib/sqlalchemy/testing/plugin/pytestplugin.py index 2ae6730bb..363a73ecc 100644 --- a/lib/sqlalchemy/testing/plugin/pytestplugin.py +++ b/lib/sqlalchemy/testing/plugin/pytestplugin.py @@ -69,6 +69,17 @@ def pytest_addoption(parser): def pytest_configure(config): + if plugin_base.exclude_tags or plugin_base.include_tags: + if config.option.markexpr: + raise ValueError( + "Can't combine explicit pytest marks with legacy options " + "such as --backend-only, --exclude-tags, etc. " + ) + config.option.markexpr = " and ".join( + list(plugin_base.include_tags) + + [f"not {tag}" for tag in plugin_base.exclude_tags] + ) + if config.pluginmanager.hasplugin("xdist"): config.pluginmanager.register(XDistHooks()) @@ -206,18 +217,43 @@ def pytest_collection_modifyitems(session, config, items): def setup_test_classes(): for test_class in test_classes: + + # transfer legacy __backend__ and __sparse_backend__ symbols + # to be markers + add_markers = set() + if getattr(test_class.cls, "__backend__", False) or getattr( + test_class.cls, "__only_on__", False + ): + add_markers = {"backend"} + elif getattr(test_class.cls, "__sparse_backend__", False): + add_markers = {"sparse_backend"} + else: + add_markers = frozenset() + + existing_markers = { + mark.name for mark in test_class.iter_markers() + } + add_markers = add_markers - existing_markers + all_markers = existing_markers.union(add_markers) + + for marker in add_markers: + test_class.add_marker(marker) + for sub_cls in plugin_base.generate_sub_tests( - test_class.cls, test_class.module + test_class.cls, test_class.module, all_markers ): if sub_cls is not test_class.cls: per_cls_dict = rebuilt_items[test_class.cls] module = test_class.getparent(pytest.Module) - for fn in collect( - pytest.Class.from_parent( - name=sub_cls.__name__, parent=module - ) - ): + + new_cls = pytest.Class.from_parent( + name=sub_cls.__name__, parent=module + ) + for marker in add_markers: + new_cls.add_marker(marker) + + for fn in collect(new_cls): per_cls_dict[fn.name].append(fn) # class requirements will sometimes need to access the DB to check @@ -573,6 +609,10 @@ class PytestFixtureFunctions(plugin_base.FixtureFunctions): def skip_test_exception(self, *arg, **kw): return pytest.skip.Exception(*arg, **kw) + @property + def add_to_marker(self): + return pytest.mark + def mark_base_test_class(self): return pytest.mark.usefixtures( "setup_class_methods", "setup_test_methods" diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index 6af2687a9..410ab26ed 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -17,6 +17,7 @@ to provide specific inclusion/exclusions. import platform +from . import config from . import exclusions from . import only_on from .. import create_engine @@ -1295,11 +1296,11 @@ class SuiteRequirements(Requirements): @property def timing_intensive(self): - return exclusions.requires_tag("timing_intensive") + return config.add_to_marker.timing_intensive @property def memory_intensive(self): - return exclusions.requires_tag("memory_intensive") + return config.add_to_marker.memory_intensive @property def threading_with_mock(self): diff --git a/pyproject.toml b/pyproject.toml index 036892d45..042bab6bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ target-version = ['py37'] [tool.pytest.ini_options] -addopts = "--tb native -v -r sfxX --maxfail=250 -p warnings -p logging" +addopts = "--tb native -v -r sfxX --maxfail=250 -p warnings -p logging --strict-markers" python_files = "test/*test_*.py" minversion = "6.2" filterwarnings = [ @@ -23,7 +23,13 @@ filterwarnings = [ "error::DeprecationWarning:test", "error::DeprecationWarning:sqlalchemy" ] - +markers = [ + "memory_intensive: memory / CPU intensive suite tests", + "mypy: mypy integration / plugin tests", + "timing_intensive: time-oriented tests that are sensitive to race conditions", + "backend: tests that should run on all backends; typically dialect-sensitive", + "sparse_backend: tests that should run on multiple backends, not necessarily all", +] [tool.pyright] include = [ diff --git a/test/aaa_profiling/test_memusage.py b/test/aaa_profiling/test_memusage.py index 2b806baf7..2fc61706c 100644 --- a/test/aaa_profiling/test_memusage.py +++ b/test/aaa_profiling/test_memusage.py @@ -252,8 +252,8 @@ class EnsureZeroed(fixtures.ORMTest): ) +@testing.add_to_marker.memory_intensive class MemUsageTest(EnsureZeroed): - __tags__ = ("memory_intensive",) __requires__ = ("cpython", "no_windows") def test_type_compile(self): @@ -347,9 +347,8 @@ class MemUsageTest(EnsureZeroed): go() +@testing.add_to_marker.memory_intensive class MemUsageWBackendTest(fixtures.MappedTest, EnsureZeroed): - - __tags__ = ("memory_intensive",) __requires__ = "cpython", "memory_process_intensive", "no_asyncio" __sparse_backend__ = True @@ -1150,8 +1149,8 @@ class MemUsageWBackendTest(fixtures.MappedTest, EnsureZeroed): metadata.drop_all(self.engine) +@testing.add_to_marker.memory_intensive class CycleTest(_fixtures.FixtureTest): - __tags__ = ("memory_intensive",) __requires__ = ("cpython", "no_windows") run_setup_mappers = "once" diff --git a/test/ext/mypy/test_mypy_plugin_py3k.py b/test/ext/mypy/test_mypy_plugin_py3k.py index 681c9d57b..cc8d8955f 100644 --- a/test/ext/mypy/test_mypy_plugin_py3k.py +++ b/test/ext/mypy/test_mypy_plugin_py3k.py @@ -10,6 +10,7 @@ from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures +@testing.add_to_marker.mypy class MypyPluginTest(fixtures.TestBase): __requires__ = ("sqlalchemy2_stubs",) @@ -77,7 +77,8 @@ allowlist_externals=sh setenv= PYTHONPATH= PYTHONNOUSERSITE=1 - MEMUSAGE=--nomemory + PYTEST_EXCLUDES=-m "not memory_intensive and not mypy" + BASECOMMAND=python -m pytest --rootdir {toxinidir} --log-info=sqlalchemy.testing WORKERS={env:TOX_WORKERS:-n4 --max-worker-restart=5} @@ -85,8 +86,8 @@ setenv= nocext: DISABLE_SQLALCHEMY_CEXT=1 cext: REQUIRE_SQLALCHEMY_CEXT=1 cov: COVERAGE={[testenv]cov_args} - backendonly: BACKENDONLY=--backend-only - memusage: MEMUSAGE='-k test_memusage' + backendonly: PYTEST_EXCLUDES="-m backend" + memusage: PYTEST_EXCLUDES="-m memory_intensive" oracle: WORKERS={env:TOX_WORKERS:-n2 --max-worker-restart=5} oracle: ORACLE={env:TOX_ORACLE:--db oracle} @@ -111,7 +112,6 @@ setenv= mssql: MSSQL={env:TOX_MSSQL:--db mssql} oracle,mssql,sqlite_file: IDENTS=--write-idents db_idents.txt - oracle,mssql,sqlite_file: MEMUSAGE=--nomemory # tox as of 2.0 blocks all environment variables from the # outside, unless they are here (or in TOX_TESTENV_PASSENV, @@ -124,7 +124,7 @@ commands= # that flag for coverage mode. nocext: sh -c "rm -f lib/sqlalchemy/*.so" - {env:BASECOMMAND} {env:WORKERS} {env:SQLITE:} {env:EXTRA_SQLITE_DRIVERS:} {env:POSTGRESQL:} {env:EXTRA_PG_DRIVERS:} {env:MYSQL:} {env:EXTRA_MYSQL_DRIVERS:} {env:ORACLE:} {env:MSSQL:} {env:BACKENDONLY:} {env:IDENTS:} {env:MEMUSAGE:} {env:COVERAGE:} {posargs} + {env:BASECOMMAND} {env:WORKERS} {env:SQLITE:} {env:EXTRA_SQLITE_DRIVERS:} {env:POSTGRESQL:} {env:EXTRA_PG_DRIVERS:} {env:MYSQL:} {env:EXTRA_MYSQL_DRIVERS:} {env:ORACLE:} {env:MSSQL:} {env:IDENTS:} {env:PYTEST_EXCLUDES:} {env:COVERAGE:} {posargs} oracle,mssql,sqlite_file: python reap_dbs.py db_idents.txt @@ -148,7 +148,7 @@ deps= patch==1.* git+https://github.com/sqlalchemy/sqlalchemy2-stubs commands = - pytest test/ext/mypy/test_mypy_plugin_py3k.py {posargs} + pytest -m mypy {posargs} # thanks to https://julien.danjou.info/the-best-flake8-extensions/ [testenv:pep8] @@ -174,7 +174,7 @@ commands = deps = {[testenv]deps} .[aiosqlite] commands= - python -m pytest {env:WORKERS} {env:SQLITE:} {env:POSTGRESQL:} {env:MYSQL:} {env:ORACLE:} {env:MSSQL:} {env:BACKENDONLY:} {env:IDENTS:} {env:MEMUSAGE:} {env:COVERAGE:} {posargs} + python -m pytest {env:WORKERS} {env:SQLITE:} {env:POSTGRESQL:} {env:MYSQL:} {env:ORACLE:} {env:MSSQL:} {env:IDENTS:} {env:PYTEST_EXCLUDES:} {env:COVERAGE:} {posargs} oracle,mssql,sqlite_file: python reap_dbs.py db_idents.txt # command run in the github action when cext are not active. @@ -182,5 +182,5 @@ commands= deps = {[testenv]deps} .[aiosqlite] commands= - python -m pytest {env:WORKERS} {env:SQLITE:} {env:POSTGRESQL:} {env:MYSQL:} {env:ORACLE:} {env:MSSQL:} {env:BACKENDONLY:} {env:IDENTS:} {env:MEMUSAGE:} {env:COVERAGE:} {posargs} + python -m pytest {env:WORKERS} {env:SQLITE:} {env:POSTGRESQL:} {env:MYSQL:} {env:ORACLE:} {env:MSSQL:} {env:IDENTS:} {env:PYTEST_EXCLUDES:} {env:COVERAGE:} {posargs} oracle,mssql,sqlite_file: python reap_dbs.py db_idents.txt |
