Projet

Général

Profil

0003-tests-add-fixture-decorator-for-db-fixture-with-glob.patch

Benjamin Dauvergne, 13 mai 2022 17:27

Télécharger (4,96 ko)

Voir les différences:

Subject: [PATCH 3/9] tests: add fixture decorator for db fixture with global
 scope (#62013)

Creating a django_db fixture which persists between tests is not easy,
this decorator simplify it and completely replace pytest.fixture for
this use case.
 tests/conftest.py | 41 ++++++++++++++++++++++++++++++++++++++++-
 tests/utils.py    | 42 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 82 insertions(+), 1 deletion(-)
tests/conftest.py
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17

  
18
import contextlib
19
import inspect
18 20
import urllib.parse
19 21
from unittest import mock
20 22

  
......
24 26
from django.contrib.auth import get_user_model
25 27
from django.core.cache import cache
26 28
from django.core.management import call_command
27
from django.db import connection
29
from django.db import connection, transaction
28 30
from django.db.migrations.executor import MigrationExecutor
29 31

  
30 32
from authentic2 import hooks as a2_hooks
......
528 530
        required_on_login=True,
529 531
        user_visible=True,
530 532
    )
533

  
534

  
535
@pytest.fixture(scope='session')
536
def scoped_db(django_db_setup, django_db_blocker):
537
    '''Scoped fixture, use like that to load some models for session/module/class scope:
538

  
539
    @pytest.fixture(scope='module')
540
    def myfixture(scoped_db):
541
        @scoped_db
542
        def f():
543
             return Model.objects.create(x=1)
544
        yield from f()
545
    '''
546

  
547
    @contextlib.contextmanager
548
    def scoped_db(func):
549
        with django_db_blocker.unblock():
550
            with transaction.atomic():
551
                try:
552
                    if inspect.isgeneratorfunction(func):
553
                        generator = func()
554
                        value = next(generator)
555
                    else:
556
                        value = func()
557
                    with django_db_blocker.block():
558
                        yield value
559
                    if inspect.isgeneratorfunction(func):
560
                        try:
561
                            next(generator)
562
                        except StopIteration:
563
                            pass
564
                        else:
565
                            raise RuntimeError(f'{func} yielded more than one time')
566
                finally:
567
                    transaction.set_rollback(True)
568

  
569
    return scoped_db
tests/utils.py
16 16
# authentic2
17 17

  
18 18
import base64
19
import functools
20
import inspect
19 21
import re
20 22
import socket
21 23
import urllib.parse
22 24
from contextlib import closing, contextmanager
23 25

  
26
import pytest
24 27
from django.contrib.messages.storage.cookie import MessageDecoder, MessageEncoder
25 28

  
26 29
try:
......
356 359
    except AttributeError:
357 360
        # xxx support legacy decoding?
358 361
        return data
362

  
363

  
364
def scoped_db_fixture(func=None, /, **kwargs):
365
    '''Create a db fixture with a scope different than 'function' the default one.'''
366
    if func is None:
367
        return functools.partial(scoped_db_fixture, **kwargs)
368
    assert kwargs.get('scope') in [
369
        'session',
370
        'module',
371
        'class',
372
    ], 'scoped_db_fixture is only usable with a non function scope'
373
    signature = inspect.signature(func)
374
    inner_parameters = []
375
    for parameter in signature.parameters:
376
        inner_parameters.append(parameter)
377
    outer_parameters = ['scoped_db'] if 'scoped_db' not in inner_parameters else []
378
    if inner_parameters and inner_parameters[0] == 'self':
379
        outer_parameters = ['self'] + outer_parameters + inner_parameters[1:]
380
    else:
381
        outer_parameters = outer_parameters + inner_parameters
382
    # build a new fixture function, inject the scoped_db fixture inside and the old function in a scoped_db context.
383
    new_function_def = f'''def f({", ".join(outer_parameters)}):
384
    if inspect.isgeneratorfunction(func):
385
        def g():
386
            yield from func({", ".join(inner_parameters)})
387
    else:
388
        def g():
389
            return func({", ".join(inner_parameters)})
390
    with scoped_db(g) as result:
391
        yield result'''
392
    new_function_bytecode = compile(new_function_def, filename=f'<scoped-db-fixture {func}>', mode='single')
393
    global_dict = {'func': func, 'inspect': inspect}
394
    eval(new_function_bytecode, global_dict)  # pylint: disable=eval-used
395
    new_function = global_dict['f']
396
    wrapped_func = functools.wraps(func)(new_function)
397
    # prevent original fixture signature to override the new fixture signature
398
    # (because of inspect.signature(follow_wrapped=True)) during pytest collection
399
    del wrapped_func.__wrapped__
400
    return pytest.fixture(**kwargs)(wrapped_func)
359
-