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
|