File: __init__.py

package info (click to toggle)
python-aioxmpp 0.12.2-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 6,152 kB
  • sloc: python: 96,969; xml: 215; makefile: 155; sh: 72
file content (466 lines) | stat: -rw-r--r-- 14,652 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
########################################################################
# File name: __init__.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program.  If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
"""
:mod:`~aioxmpp.e2etest` --- Framework for writing integration tests for :mod:`aioxmpp`
######################################################################################

This subpackage provides utilities for writing end-to-end or intgeration tests
for :mod:`aioxmpp` components.

.. warning::

   For now, the API of this subpackage is classified as internal. Please do not
   test your external components using this API, as it is experimental and
   subject to change.

Overview
========

The basic concept is that tests are written like normal unittests. However,
tests are written by inheriting classes from :class:`aioxmpp.e2etest.TestCase`
instead of :mod:`unittest.TestCase`. :class:`.e2etest.TestCase` has the
:attr:`~.e2etest.TestCase.provisioner` attribute which provides access to a
:class:`.provision.Provisioner` instance.

Provisioners are objects which provide a way to obtain a connected XMPP client.
The JID to which the client is bound is unspecified; however, each client gets
a unique bare JID and the clients are able to communicate with each other. In
addition, provisioners provide information about the environment in which the
clients act. This includes providing JIDs of entities implementing specific
protocols or features. The details are explained in the documentation of the
:class:`~.provision.Provisioner` base class.

By default, tests which are written with :class:`.e2etest.TestCase` are skipped
when using the normal test runners. This is because the provisioners need to be
configured; this is handled using a custom nosetests plugin which is not loaded
by default (for good reasons). To run the tests, use (instead of the normal
``nosetests3`` binary):

.. code-block:: console

   $ python3 -m aioxmpp.e2etest

The command line interface is identical to the one of ``nosetests3``, except
that additional options are provided to configure the plugin. In fact,
:mod:`aioxmpp.e2etest` is simply a nose test runner with an additional plugin.

By default, the configuration is read from ``./.local/e2etest.ini``. For
details on configuring the provisioners, see :ref:`the developer guide
<dg-end-to-end-tests>`.

Main API
========

Decorators for test methods
---------------------------

The following decorators can be used on test methods (including ``setUp`` and
``tearDown``):

.. autodecorator:: require_feature

.. autodecorator:: require_identity

.. autodecorator:: require_feature_subset

.. autodecorator:: skip_with_quirk

General decorators
------------------

.. autodecorator:: blocking()

.. autodecorator:: blocking_timed()

.. autodecorator:: blocking_with_timeout

Class for test cases
--------------------

.. autoclass:: TestCase

.. currentmodule:: aioxmpp.e2etest.provision

Provisioners
============

.. autoclass:: Provisioner

.. autoclass:: AnonymousProvisioner()

.. autoclass:: AnyProvisioner()

.. autoclass:: StaticPasswordProvisioner()

.. currentmodule:: aioxmpp.e2etest

.. autoclass:: Quirk

.. currentmodule:: aioxmpp.e2etest.provision

Helper functions
----------------

.. autofunction:: discover_server_features

.. autofunction:: configure_tls_config

.. autofunction:: configure_quirks
"""  # NOQA: E501
import asyncio
import configparser
import functools
import importlib
import logging
import os
import unittest

from nose.plugins import Plugin

from ..testutils import get_timeout
from .utils import blocking
from .provision import Quirk  # NOQA: F401


provisioner = None
config = None
timeout = get_timeout(1.0)


def require_feature(feature_var, argname=None, *, multiple=False):
    """
    :param feature_var: :xep:`30` feature ``var`` of the required feature
    :type feature_var: :class:`str`
    :param argname: Optional argument name to pass the :class:`FeatureInfo` to
    :type argname: :class:`str` or :data:`None`
    :param multiple: If true, all peers are returned instead of a random one.
    :type multiple: :class:`bool`

    Before running the function, it is tested that the feature specified by
    `feature_var` is provided in the environment of the current provisioner. If
    it is not, :class:`unittest.SkipTest` is raised to skip the test.

    If the feature is available, the :class:`FeatureInfo` instance is passed to
    the decorated function. If `argname` is :data:`None`, the feature info is
    passed as additional positional argument. otherwise, it is passed as
    keyword argument using the `argname`.

    If `multiple` is true, all peers supporting the given feature are passed
    in a set. Otherwise, only a random peer is returned.

    This decorator can be used on test methods, but not on test classes. If you
    want to skip all tests in a class, apply the decorator to the ``setUp``
    method.
    """
    if isinstance(feature_var, str):
        feature_var = [feature_var]

    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            global provisioner
            if multiple:
                arg = provisioner.get_feature_providers(feature_var)
                has_provider = bool(arg)
            else:
                arg = provisioner.get_feature_provider(feature_var)
                has_provider = arg is not None
            if not has_provider:
                raise unittest.SkipTest(
                    "provisioner does not provide a peer with "
                    "{!r}".format(feature_var)
                )

            if argname is None:
                args = args+(arg,)
            else:
                kwargs[argname] = arg

            return f(*args, **kwargs)
        return wrapper

    return decorator


def require_identity(category, type_, argname=None):
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            global provisioner
            arg = provisioner.get_identity_provider(category, type_)
            has_provider = arg is not None
            if not has_provider:
                raise unittest.SkipTest(
                    "provisioner does not provide a peer with a "
                    "{!r} identity".format((category, type_))
                )

            if argname is None:
                args = args+(arg,)
            else:
                kwargs[argname] = arg

            return f(*args, **kwargs)
        return wrapper

    return decorator


def require_feature_subset(feature_vars, required_subset=[]):
    required_subset = set(required_subset)
    feature_vars = set(feature_vars) | required_subset

    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            global provisioner
            jid, subset = provisioner.get_feature_subset_provider(
                feature_vars,
                required_subset
            )
            if jid is None:
                raise unittest.SkipTest(
                    "no peer could provide a subset of {!r} with at least "
                    "{!r}".format(
                        feature_vars,
                        required_subset,
                    )
                )

            return f(*(args+(jid, feature_vars)),
                     **kwargs)
        return wrapper

    return decorator


def require_pep(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        global provisioner
        if not provisioner.has_pep():
            raise unittest.SkipTest(
                "the provisioned account does not support PEP",
            )

        return f(*args, **kwargs)
    return wrapper


def skip_with_quirk(quirk):
    """
    :param quirk: The quirk to skip on
    :type quirk: :class:`Quirks`

    If the provisioner indicates that the environment has the given `quirk`,
    the test is skipped.

    This decorator can be used on test methods, but not on test classes. If you
    want to skip all tests in a class, apply the decorator to the ``setUp``
    method.
    """

    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            global provisioner
            if provisioner.has_quirk(quirk):
                raise unittest.SkipTest(
                    "provisioner has quirk {!r}".format(quirk)
                )
            return f(*args, **kwargs)
        return wrapper

    return decorator


def blocking_with_timeout(timeout):
    """
    The decorated coroutine function is run using the
    :meth:`~asyncio.AbstractEventLoop.run_until_complete` method of the current
    (at the time of call) event loop.

    If the execution takes longer than `timeout` seconds,
    :class:`asyncio.TimeoutError` is raised.

    The decorated function behaves like a normal function and is not a
    coroutine function.

    This decorator must be applied to a coroutine function (or method).
    """

    def decorator(f):
        @blocking
        @functools.wraps(f)
        async def wrapper(*args, **kwargs):
            return await asyncio.wait_for(f(*args, **kwargs), timeout)
        return wrapper
    return decorator


def blocking_timed(f):
    """
    Like :func:`blocking_with_timeout`, the decorated coroutine function is
    executed using :meth:`asyncio.AbstractEventLoop.run_until_complete` with a
    timeout, but the timeout is configured in the end-to-end test configuration
    (see :ref:`dg-end-to-end-tests`).

    This is the recommended decorator for any test function or method, to
    prevent the tests from hanging when anythin goes wrong. The timeout is
    under control of the provisioner configuration, which means that it can be
    adapted to different setups (for example, running against an XMPP server in
    the internet will be slower than if it runs on localhost).

    The decorated function behaves like a normal function and is not a
    coroutine function.

    This decorator must be applied to a coroutine function (or method).
    """
    @blocking
    @functools.wraps(f)
    async def wrapper(*args, **kwargs):
        global timeout
        await asyncio.wait_for(f(*args, **kwargs), timeout)
    return wrapper


@blocking
async def setup_package():
    global provisioner, config, timeout
    if config is None:
        # AioxmppPlugin is not used -> skip all e2e tests
        for subclass in TestCase.__subclasses__():
            # XXX: this depends on unittest implementation details :)
            subclass.__unittest_skip__ = True
            subclass.__unittest_skip_why__ = \
                "this is not the aioxmpp test runner"
        return

    timeout = config.getfloat("global", "timeout", fallback=timeout)

    provisioner_name = config.get("global", "provisioner")
    module_path, class_name = provisioner_name.rsplit(".", 1)
    mod = importlib.import_module(module_path)
    cls_ = getattr(mod, class_name)

    section = config[provisioner_name]
    provisioner = cls_()
    provisioner.configure(section)
    await provisioner.initialise()


def teardown_package():
    global provisioner, config
    if config is None:
        return

    loop = asyncio.get_event_loop()
    loop.run_until_complete(provisioner.finalise())
    loop.close()


class E2ETestPlugin(Plugin):
    name = "aioxmpp-e2e"

    def options(self, options, env=os.environ):
        options.add_option(
            "--e2etest-config",
            dest="aioxmpp_e2e_config",
            metavar="FILE",
            default=".local/e2etest.ini",
            help="Configuration file for end-to-end tests "
            "(default: .local/e2etest.ini)"
        )
        options.add_option(
            "--e2etest-record",
            dest="aioxmpp_e2e_record",
            metavar="FILE",
            default=None,
            help="A file to write a transcript to"
        )
        options.add_option(
            "--e2etest-only",
            dest="aioxmpp_e2e_only",
            action="store_true",
            default=False,
            help="If set, only E2E tests will be executed."
        )

    def configure(self, options, conf):
        self.enabled = True
        global config
        config = configparser.ConfigParser()
        with open(options.aioxmpp_e2e_config, "r") as f:
            config.read_file(f)

        if options.aioxmpp_e2e_record:
            handler = logging.FileHandler(options.aioxmpp_e2e_record, "w")
            formatter = logging.Formatter(
                "%(name)s: %(levelname)s: %(message)s",
                style="%"
            )
            handler.setFormatter(formatter)
            logger = logging.getLogger("aioxmpp.e2etest.provision")
            logger.addHandler(handler)

        self.__only_e2etest = options.aioxmpp_e2e_only

    @blocking
    async def beforeTest(self, test):
        global provisioner
        if self.__only_e2etest and not isinstance(test.test, TestCase):
            raise unittest.SkipTest("not an e2etest")
        if provisioner is not None:
            await provisioner.setup()

    @blocking
    async def afterTest(self, test):
        global provisioner
        if provisioner is not None:
            await provisioner.teardown()


class TestCase(unittest.TestCase):
    """
    A subclass of :class:`unittest.TestCase` for end-to-end test cases.

    This subclass provides a single additional attribute:

    .. autoattribute:: provisioner
    """

    @property
    def provisioner(self):
        """
        This is the configured :class:`.provision.Provisioner` instance.

        If no provisioner is configured (for example because the e2etest nose
        plugin is not loaded), this reads as :data:`None`.

        .. note::

           Under nosetests and the vanilla unittest runner, tests inheriting
           from :class:`TestCase` are automatically skipped if
           :attr:`provisioner` is :data:`None`.
        """
        global provisioner
        return provisioner