Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
plugin_base.py780 linesDownload Raw Back to plugin
1# testing/plugin/plugin_base.py
2# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7# mypy: ignore-errors
8
9
10from __future__ import annotations
11
12import abc
13from argparse import Namespace
14import configparser
15import logging
16import os
17from pathlib import Path
18import re
19import sys
20from typing import Any
21
22from sqlalchemy.testing import asyncio
23
24"""Testing extensions.
25
26this module is designed to work as a testing-framework-agnostic library,
27created so that multiple test frameworks can be supported at once
28(mostly so that we can migrate to new ones). The current target
29is pytest.
30
31"""
32
33# flag which indicates we are in the SQLAlchemy testing suite,
34# and not that of Alembic or a third party dialect.
35bootstrapped_as_sqlalchemy = False
36
37log = logging.getLogger("sqlalchemy.testing.plugin_base")
38
39# late imports
40fixtures = None
41engines = None
42exclusions = None
43warnings = None
44profiling = None
45provision = None
46assertions = None
47requirements = None
48config = None
49testing = None
50util = None
51file_config = None
52
53logging = None
54include_tags = set()
55exclude_tags = set()
56options: Namespace = None  # type: ignore
57
58
59def setup_options(make_option):
60    make_option(
61        "--log-info",
62        action="callback",
63        type=str,
64        callback=_log,
65        help="turn on info logging for <LOG> (multiple OK)",
66    )
67    make_option(
68        "--log-debug",
69        action="callback",
70        type=str,
71        callback=_log,
72        help="turn on debug logging for <LOG> (multiple OK)",
73    )
74    make_option(
75        "--db",
76        action="append",
77        type=str,
78        dest="db",
79        help="Use prefab database uri. Multiple OK, "
80        "first one is run by default.",
81    )
82    make_option(
83        "--dbs",
84        action="callback",
85        zeroarg_callback=_list_dbs,
86        help="List available prefab dbs",
87    )
88    make_option(
89        "--dburi",
90        action="append",
91        type=str,
92        dest="dburi",
93        help="Database uri.  Multiple OK, first one is run by default.",
94    )
95    make_option(
96        "--dbdriver",
97        action="append",
98        type=str,
99        dest="dbdriver",
100        help="Additional database drivers to include in tests.  "
101        "These are linked to the existing database URLs by the "
102        "provisioning system.",
103    )
104    make_option(
105        "--dropfirst",
106        action="store_true",
107        dest="dropfirst",
108        help="Drop all tables in the target database first",
109    )
110    make_option(
111        "--disable-asyncio",
112        action="store_true",
113        help="disable test / fixtures / provisoning running in asyncio",
114    )
115    make_option(
116        "--backend-only",
117        action="callback",
118        zeroarg_callback=_set_tag_include("backend"),
119        help=(
120            "Run only tests marked with __backend__ or __sparse_backend__; "
121            "this is now equivalent to the pytest -m backend mark expression"
122        ),
123    )
124    make_option(
125        "--nomemory",
126        action="callback",
127        zeroarg_callback=_set_tag_exclude("memory_intensive"),
128        help="Don't run memory profiling tests; "
129        "this is now equivalent to the pytest -m 'not memory_intensive' "
130        "mark expression",
131    )
132    make_option(
133        "--notimingintensive",
134        action="callback",
135        zeroarg_callback=_set_tag_exclude("timing_intensive"),
136        help="Don't run timing intensive tests; "
137        "this is now equivalent to the pytest -m 'not timing_intensive' "
138        "mark expression",
139    )
140    make_option(
141        "--nomypy",
142        action="callback",
143        zeroarg_callback=_set_tag_exclude("mypy"),
144        help="Don't run mypy typing tests; "
145        "this is now equivalent to the pytest -m 'not mypy' mark expression",
146    )
147    make_option(
148        "--profile-sort",
149        type=str,
150        default="cumulative",
151        dest="profilesort",
152        help="Type of sort for profiling standard output",
153    )
154    make_option(
155        "--profile-dump",
156        type=str,
157        dest="profiledump",
158        help="Filename where a single profile run will be dumped",
159    )
160    make_option(
161        "--low-connections",
162        action="store_true",
163        dest="low_connections",
164        help="Use a low number of distinct connections - "
165        "i.e. for Oracle TNS",
166    )
167    make_option(
168        "--write-idents",
169        type=str,
170        dest="write_idents",
171        help="write out generated follower idents to <file>, "
172        "when -n<num> is used",
173    )
174    make_option(
175        "--requirements",
176        action="callback",
177        type=str,
178        callback=_requirements_opt,
179        help="requirements class for testing, overrides setup.cfg",
180    )
181    make_option(
182        "--include-tag",
183        action="callback",
184        callback=_include_tag,
185        type=str,
186        help="Include tests with tag <tag>; "
187        "legacy, use pytest -m 'tag' instead",
188    )
189    make_option(
190        "--exclude-tag",
191        action="callback",
192        callback=_exclude_tag,
193        type=str,
194        help="Exclude tests with tag <tag>; "
195        "legacy, use pytest -m 'not tag' instead",
196    )
197    make_option(
198        "--write-profiles",
199        action="store_true",
200        dest="write_profiles",
201        default=False,
202        help="Write/update failing profiling data.",
203    )
204    make_option(
205        "--force-write-profiles",
206        action="store_true",
207        dest="force_write_profiles",
208        default=False,
209        help="Unconditionally write/update profiling data.",
210    )
211    make_option(
212        "--dump-pyannotate",
213        type=str,
214        dest="dump_pyannotate",
215        help="Run pyannotate and dump json info to given file",
216    )
217    make_option(
218        "--mypy-extra-test-path",
219        type=str,
220        action="append",
221        default=[],
222        dest="mypy_extra_test_paths",
223        help="Additional test directories to add to the mypy tests. "
224        "This is used only when running mypy tests. Multiple OK",
225    )
226    # db specific options
227    make_option(
228        "--postgresql-templatedb",
229        type=str,
230        help="name of template database to use for PostgreSQL "
231        "CREATE DATABASE (defaults to current database)",
232    )
233    make_option(
234        "--oracledb-thick-mode",
235        action="store_true",
236        help="enables the 'thick mode' when testing with oracle+oracledb",
237    )
238
239
240def configure_follower(follower_ident):
241    """Configure required state for a follower.
242
243    This invokes in the parent process and typically includes
244    database creation.
245
246    """
247    from sqlalchemy.testing import provision
248
249    provision.FOLLOWER_IDENT = follower_ident
250
251
252def memoize_important_follower_config(dict_):
253    """Store important configuration we will need to send to a follower.
254
255    This invokes in the parent process after normal config is set up.
256
257    Hook is currently not used.
258
259    """
260
261
262def restore_important_follower_config(dict_):
263    """Restore important configuration needed by a follower.
264
265    This invokes in the follower process.
266
267    Hook is currently not used.
268
269    """
270
271
272def read_config(root_path):
273    global file_config
274    file_config = configparser.ConfigParser()
275    file_config.read(
276        [str(root_path / "setup.cfg"), str(root_path / "test.cfg")]
277    )
278
279
280def pre_begin(opt):
281    """things to set up early, before coverage might be setup."""
282    global options
283    options = opt
284    for fn in pre_configure:
285        fn(options, file_config)
286
287
288def set_coverage_flag(value):
289    options.has_coverage = value
290
291
292def post_begin():
293    """things to set up later, once we know coverage is running."""
294    # Lazy setup of other options (post coverage)
295    for fn in post_configure:
296        fn(options, file_config)
297
298    # late imports, has to happen after config.
299    global util, fixtures, engines, exclusions, assertions, provision
300    global warnings, profiling, config, testing
301    from sqlalchemy import testing  # noqa
302    from sqlalchemy.testing import fixtures, engines, exclusions  # noqa
303    from sqlalchemy.testing import assertions, warnings, profiling  # noqa
304    from sqlalchemy.testing import config, provision  # noqa
305    from sqlalchemy import util  # noqa
306
307    warnings.setup_filters()
308
309
310def _log(opt_str, value, parser):
311    global logging
312    if not logging:
313        import logging
314
315        logging.basicConfig()
316
317    if opt_str.endswith("-info"):
318        logging.getLogger(value).setLevel(logging.INFO)
319    elif opt_str.endswith("-debug"):
320        logging.getLogger(value).setLevel(logging.DEBUG)
321
322
323def _list_dbs(*args):
324    if file_config is None:
325        # assume the current working directory is the one containing the
326        # setup file
327        read_config(Path.cwd())
328    print("Available --db options (use --dburi to override)")
329    for macro in sorted(file_config.options("db")):
330        print("%20s\t%s" % (macro, file_config.get("db", macro)))
331    sys.exit(0)
332
333
334def _requirements_opt(opt_str, value, parser):
335    _setup_requirements(value)
336
337
338def _set_tag_include(tag):
339    def _do_include_tag(opt_str, value, parser):
340        _include_tag(opt_str, tag, parser)
341
342    return _do_include_tag
343
344
345def _set_tag_exclude(tag):
346    def _do_exclude_tag(opt_str, value, parser):
347        _exclude_tag(opt_str, tag, parser)
348
349    return _do_exclude_tag
350
351
352def _exclude_tag(opt_str, value, parser):
353    exclude_tags.add(value.replace("-", "_"))
354
355
356def _include_tag(opt_str, value, parser):
357    include_tags.add(value.replace("-", "_"))
358
359
360pre_configure = []
361post_configure = []
362
363
364def pre(fn):
365    pre_configure.append(fn)
366    return fn
367
368
369def post(fn):
370    post_configure.append(fn)
371    return fn
372
373
374@pre
375def _setup_options(opt, file_config):
376    global options
377    options = opt
378
379
380@pre
381def _register_sqlite_numeric_dialect(opt, file_config):
382    from sqlalchemy.dialects import registry
383
384    registry.register(
385        "sqlite.pysqlite_numeric",
386        "sqlalchemy.dialects.sqlite.pysqlite",
387        "_SQLiteDialect_pysqlite_numeric",
388    )
389    registry.register(
390        "sqlite.pysqlite_dollar",
391        "sqlalchemy.dialects.sqlite.pysqlite",
392        "_SQLiteDialect_pysqlite_dollar",
393    )
394
395
396@post
397def __ensure_cext(opt, file_config):
398    if os.environ.get("REQUIRE_SQLALCHEMY_CEXT", "0") == "1":
399        from sqlalchemy.util import has_compiled_ext
400
401        try:
402            has_compiled_ext(raise_=True)
403        except ImportError as err:
404            raise AssertionError(
405                "REQUIRE_SQLALCHEMY_CEXT is set but can't import the "
406                "cython extensions"
407            ) from err
408
409
410@post
411def _init_symbols(options, file_config):
412    from sqlalchemy.testing import config
413
414    config._fixture_functions = _fixture_fn_class()
415
416
417@pre
418def _set_disable_asyncio(opt, file_config):
419    if opt.disable_asyncio:
420        asyncio.ENABLE_ASYNCIO = False
421
422
423@post
424def _engine_uri(options, file_config):
425    from sqlalchemy import testing
426    from sqlalchemy.testing import config
427    from sqlalchemy.testing import provision
428    from sqlalchemy.engine import url as sa_url
429
430    if options.dburi:
431        db_urls = list(options.dburi)
432    else:
433        db_urls = []
434
435    extra_drivers = options.dbdriver or []
436
437    if options.db:
438        for db_token in options.db:
439            for db in re.split(r"[,\s]+", db_token):
440                if db not in file_config.options("db"):
441                    raise RuntimeError(
442                        "Unknown URI specifier '%s'.  "
443                        "Specify --dbs for known uris." % db
444                    )
445                else:
446                    db_urls.append(file_config.get("db", db))
447
448    if not db_urls:
449        db_urls.append(file_config.get("db", "default"))
450
451    config._current = None
452
453    if options.write_idents and provision.FOLLOWER_IDENT:
454        for db_url in [sa_url.make_url(db_url) for db_url in db_urls]:
455            with open(options.write_idents, "a") as file_:
456                file_.write(
457                    f"{provision.FOLLOWER_IDENT} "
458                    f"{db_url.render_as_string(hide_password=False)}\n"
459                )
460
461    expanded_urls = list(provision.generate_db_urls(db_urls, extra_drivers))
462
463    for db_url in expanded_urls:
464        log.info("Adding database URL: %s", db_url)
465
466        cfg = provision.setup_config(
467            db_url, options, file_config, provision.FOLLOWER_IDENT
468        )
469        if not config._current:
470            cfg.set_as_current(cfg, testing)
471
472
473@post
474def _requirements(options, file_config):
475    requirement_cls = file_config.get("sqla_testing", "requirement_cls")
476    _setup_requirements(requirement_cls)
477
478
479def _setup_requirements(argument):
480    from sqlalchemy.testing import config
481    from sqlalchemy import testing
482
483    modname, clsname = argument.split(":")
484
485    # importlib.import_module() only introduced in 2.7, a little
486    # late
487    mod = __import__(modname)
488    for component in modname.split(".")[1:]:
489        mod = getattr(mod, component)
490    req_cls = getattr(mod, clsname)
491
492    config.requirements = testing.requires = req_cls()
493
494    config.bootstrapped_as_sqlalchemy = bootstrapped_as_sqlalchemy
495
496
497@post
498def _prep_testing_database(options, file_config):
499    from sqlalchemy.testing import config
500
501    if options.dropfirst:
502        from sqlalchemy.testing import provision
503
504        for cfg in config.Config.all_configs():
505            provision.drop_all_schema_objects(cfg, cfg.db)
506
507
508@post
509def _post_setup_options(opt, file_config):
510    from sqlalchemy.testing import config
511
512    config.options = options
513    config.file_config = file_config
514
515
516@post
517def _setup_profiling(options, file_config):
518    from sqlalchemy.testing import profiling
519
520    profiling._profile_stats = profiling.ProfileStatsFile(
521        file_config.get("sqla_testing", "profile_file"),
522        sort=options.profilesort,
523        dump=options.profiledump,
524    )
525
526
527def want_class(name, cls):
528    if not issubclass(cls, fixtures.TestBase):
529        return False
530    elif name.startswith("_"):
531        return False
532    else:
533        return True
534
535
536def want_method(cls, fn):
537    if not fn.__name__.startswith("test_"):
538        return False
539    elif fn.__module__ is None:
540        return False
541    else:
542        return True
543
544
545def generate_sub_tests(cls, module, markers):
546    if "backend" in markers or "sparse_backend" in markers:
547        sparse = "sparse_backend" in markers
548        for cfg in _possible_configs_for_cls(cls, sparse=sparse):
549            orig_name = cls.__name__
550
551            # we can have special chars in these names except for the
552            # pytest junit plugin, which is tripped up by the brackets
553            # and periods, so sanitize
554
555            alpha_name = re.sub(r"[_\[\]\.]+", "_", cfg.name)
556            alpha_name = re.sub(r"_+$", "", alpha_name)
557            name = "%s_%s" % (cls.__name__, alpha_name)
558            subcls = type(
559                name,
560                (cls,),
561                {"_sa_orig_cls_name": orig_name, "__only_on_config__": cfg},
562            )
563            setattr(module, name, subcls)
564            yield subcls
565    else:
566        yield cls
567
568
569def start_test_class_outside_fixtures(cls):
570    _do_skips(cls)
571    _setup_engine(cls)
572
573
574def stop_test_class(cls):
575    # close sessions, immediate connections, etc.
576    fixtures.stop_test_class_inside_fixtures(cls)
577
578    # close outstanding connection pool connections, dispose of
579    # additional engines
580    engines.testing_reaper.stop_test_class_inside_fixtures()
581
582
583def stop_test_class_outside_fixtures(cls):
584    engines.testing_reaper.stop_test_class_outside_fixtures()
585    provision.stop_test_class_outside_fixtures(config, config.db, cls)
586    try:
587        if not options.low_connections:
588            assertions.global_cleanup_assertions()
589    finally:
590        _restore_engine()
591
592
593def _restore_engine():
594    if config._current:
595        config._current.reset(testing)
596
597
598def final_process_cleanup():
599    engines.testing_reaper.final_cleanup()
600    assertions.global_cleanup_assertions()
601    _restore_engine()
602
603
604def _setup_engine(cls):
605    if getattr(cls, "__engine_options__", None):
606        opts = dict(cls.__engine_options__)
607        opts["scope"] = "class"
608        eng = engines.testing_engine(options=opts)
609        config._current.push_engine(eng, testing)
610
611
612def before_test(test, test_module_name, test_class, test_name):
613    # format looks like:
614    # "test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause"
615
616    name = getattr(test_class, "_sa_orig_cls_name", test_class.__name__)
617
618    id_ = "%s.%s.%s" % (test_module_name, name, test_name)
619
620    profiling._start_current_test(id_)
621
622
623def after_test(test):
624    fixtures.after_test()
625    engines.testing_reaper.after_test()
626
627
628def after_test_fixtures(test):
629    engines.testing_reaper.after_test_outside_fixtures(test)
630
631
632def _possible_configs_for_cls(cls, reasons=None, sparse=False):
633    all_configs = set(config.Config.all_configs())
634
635    if cls.__unsupported_on__:
636        spec = exclusions.db_spec(*cls.__unsupported_on__)
637        for config_obj in list(all_configs):
638            if spec(config_obj):
639                all_configs.remove(config_obj)
640
641    if getattr(cls, "__only_on__", None):
642        spec = exclusions.db_spec(*util.to_list(cls.__only_on__))
643        for config_obj in list(all_configs):
644            if not spec(config_obj):
645                all_configs.remove(config_obj)
646
647    if getattr(cls, "__only_on_config__", None):
648        all_configs.intersection_update([cls.__only_on_config__])
649
650    if hasattr(cls, "__requires__"):
651        requirements = config.requirements
652        for config_obj in list(all_configs):
653            for requirement in cls.__requires__:
654                check = getattr(requirements, requirement)
655
656                skip_reasons = check.matching_config_reasons(config_obj)
657                if skip_reasons:
658                    all_configs.remove(config_obj)
659                    if reasons is not None:
660                        reasons.extend(skip_reasons)
661                    break
662
663    if hasattr(cls, "__prefer_requires__"):
664        non_preferred = set()
665        requirements = config.requirements
666        for config_obj in list(all_configs):
667            for requirement in cls.__prefer_requires__:
668                check = getattr(requirements, requirement)
669
670                if not check.enabled_for_config(config_obj):
671                    non_preferred.add(config_obj)
672        if all_configs.difference(non_preferred):
673            all_configs.difference_update(non_preferred)
674
675    if sparse:
676        # pick only one config from each base dialect
677        # sorted so we get the same backend each time selecting the highest
678        # server version info.
679        per_dialect = {}
680        for cfg in reversed(
681            sorted(
682                all_configs,
683                key=lambda cfg: (
684                    cfg.db.name,
685                    cfg.db.driver,
686                    cfg.db.dialect.server_version_info,
687                ),
688            )
689        ):
690            db = cfg.db.name
691            if db not in per_dialect:
692                per_dialect[db] = cfg
693        return per_dialect.values()
694
695    return all_configs
696
697
698def _do_skips(cls):
699    reasons = []
700    all_configs = _possible_configs_for_cls(cls, reasons)
701
702    if getattr(cls, "__skip_if__", False):
703        for c in getattr(cls, "__skip_if__"):
704            if c():
705                config.skip_test(
706                    "'%s' skipped by %s" % (cls.__name__, c.__name__)
707                )
708
709    if not all_configs:
710        msg = "'%s.%s' unsupported on any DB implementation %s%s" % (
711            cls.__module__,
712            cls.__name__,
713            ", ".join(
714                "'%s(%s)+%s'"
715                % (
716                    config_obj.db.name,
717                    ".".join(
718                        str(dig)
719                        for dig in exclusions._server_version(config_obj.db)
720                    ),
721                    config_obj.db.driver,
722                )
723                for config_obj in config.Config.all_configs()
724            ),
725            ", ".join(reasons),
726        )
727        config.skip_test(msg)
728    elif hasattr(cls, "__prefer_backends__"):
729        non_preferred = set()
730        spec = exclusions.db_spec(*util.to_list(cls.__prefer_backends__))
731        for config_obj in all_configs:
732            if not spec(config_obj):
733                non_preferred.add(config_obj)
734        if all_configs.difference(non_preferred):
735            all_configs.difference_update(non_preferred)
736
737    if config._current not in all_configs:
738        _setup_config(all_configs.pop(), cls)
739
740
741def _setup_config(config_obj, ctx):
742    config._current.push(config_obj, testing)
743
744
745class FixtureFunctions(abc.ABC):
746    @abc.abstractmethod
747    def skip_test_exception(self, *arg, **kw):
748        raise NotImplementedError()
749
750    @abc.abstractmethod
751    def combinations(self, *args, **kw):
752        raise NotImplementedError()
753
754    @abc.abstractmethod
755    def param_ident(self, *args, **kw):
756        raise NotImplementedError()
757
758    @abc.abstractmethod
759    def fixture(self, *arg, **kw):
760        raise NotImplementedError()
761
762    def get_current_test_name(self):
763        raise NotImplementedError()
764
765    @abc.abstractmethod
766    def mark_base_test_class(self) -> Any:
767        raise NotImplementedError()
768
769    @abc.abstractproperty
770    def add_to_marker(self):
771        raise NotImplementedError()
772
773
774_fixture_fn_class = None
775
776
777def set_fixture_functions(fixture_fn_class):
778    global _fixture_fn_class
779    _fixture_fn_class = fixture_fn_class
780 
codekingpro/portable-devtools · Team Ai