diff options
| author | drebs <drebs@riseup.net> | 2017-09-17 12:08:25 -0300 | 
|---|---|---|
| committer | drebs <drebs@riseup.net> | 2017-09-17 15:50:55 -0300 | 
| commit | cfff46ff9becdbe5cf48816870e625ed253ecc57 (patch) | |
| tree | 8d239e4499f559d86ed17ea3632008303b25d485 /tests/conftest.py | |
| parent | f29abe28bd778838626d12fcabe3980a8ce4fa8c (diff) | |
[refactor] move tests to root of repository
Tests entrypoint was in a testing/ subfolder in the root of the
repository. This was made mainly because we had some common files for
tests and we didn't want to ship them (files in testing/test_soledad,
which is itself a python package. This sometimes causes errors when
loading tests (it seems setuptools is confused with having one python
package in a subdirectory of another).
This commit moves the tests entrypoint to the root of the repository.
Closes: #8952
Diffstat (limited to 'tests/conftest.py')
| -rw-r--r-- | tests/conftest.py | 398 | 
1 files changed, 398 insertions, 0 deletions
| diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..d3a39289 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,398 @@ +import glob +import base64 +import json +import os +import pytest +import re +import random +import requests +import signal +import socket +import subprocess +import sys +import time +import urlparse + +from hashlib import sha512 +from six.moves.urllib.parse import urljoin +from uuid import uuid4 + +from leap.soledad.common.couch import CouchDatabase +from leap.soledad.client import Soledad + + +def _select_subdir(subdir, blacklist, items): + +    # allow blacklisted subdir if explicited in command line +    if subdir and subdir in blacklist: +        blacklist.remove(subdir) + +    # determine blacklisted subdirs +    dirname = os.path.dirname(__file__) +    blacklisted_subdirs = map(lambda s: os.path.join(dirname, s), blacklist) + +    # determine base path for selected tests +    path = dirname +    if subdir: +        path = os.path.join(dirname, subdir) + +    # remove tests from blacklisted subdirs +    selected = [] +    deselected = [] +    for item in items: +        filename = item.module.__file__ +        blacklisted = any( +            map(lambda s: filename.startswith(s), blacklisted_subdirs)) +        if blacklisted or not filename.startswith(path): +            deselected.append(item) +        else: +            selected.append(item) + +    return selected, deselected + + +def pytest_collection_modifyitems(items, config): + +    # mark tests that depend on couchdb +    marker = getattr(pytest.mark, 'needs_couch') +    for item in items: +        if 'soledad/testing/tests/couch/' in item.module.__file__: +            item.add_marker(marker) + +    # select/deselect tests based on a blacklist and the subdir option given in +    # command line +    blacklist = ['benchmarks', 'responsiveness'] +    subdir = config.getoption('subdir') +    selected, deselected = _select_subdir(subdir, blacklist, items) +    config.hook.pytest_deselected(items=deselected) +    items[:] = selected + + +# +# default options for all tests +# + +DEFAULT_PASSPHRASE = '123' + +DEFAULT_URL = 'http://127.0.0.1:2424' +DEFAULT_PRIVKEY = 'soledad_privkey.pem' +DEFAULT_CERTKEY = 'soledad_certkey.pem' +DEFAULT_TOKEN = 'an-auth-token' + + +def pytest_addoption(parser): +    parser.addoption( +        "--couch-url", type="string", default="http://127.0.0.1:5984", +        help="the url for the couch server to be used during tests") + +    # the following options are only used in benchmarks, but has to be defined +    # here due to how pytest discovers plugins during startup. +    parser.addoption( +        "--watch-memory", default=False, action="store_true", +        help="whether to monitor memory percentages during test run. " +             "**Warning**: enabling this will impact the time taken and the " +             "CPU used by the benchmarked code, so use with caution!") + +    parser.addoption( +        "--soledad-server-url", type="string", default=None, +        help="Soledad Server URL. A local server will be started if and only " +             "if  no URL is passed.") + +    # the following option is only used in responsiveness tests, but has to be +    # defined here due to how pytest discovers plugins during startup. +    parser.addoption( +        "--elasticsearch-url", type="string", default=None, +        help="the url for posting responsiveness results to elasticsearch") + +    parser.addoption( +        "--subdir", type="string", default=None, +        help="select only tests from a certain subdirectory of ./tests/") + + +def _request(method, url, data=None, do=True): +    if do: +        method = getattr(requests, method) +        method(url, data=data) +    else: +        cmd = 'curl --netrc -X %s %s' % (method.upper(), url) +        if data: +            cmd += ' -d "%s"' % json.dumps(data) +        print cmd + + +@pytest.fixture +def couch_url(request): +    url = request.config.option.couch_url +    request.cls.couch_url = url + + +@pytest.fixture +def method_tmpdir(request, tmpdir): +    request.instance.tempdir = tmpdir.strpath + + +# +# remote_db fixture: provides an empty database for a given user in a per +# function scope. +# + +class UserDatabase(object): + +    def __init__(self, url, uuid, create=True): +        self._remote_db_url = urljoin(url, 'user-%s' % uuid) +        self._create = create + +    def setup(self): +        if self._create: +            return CouchDatabase.open_database( +                url=self._remote_db_url, create=True, replica_uid=None) +        else: +            _request('put', self._remote_db_url, do=False) + +    def teardown(self): +        _request('delete', self._remote_db_url, do=self._create) + + +@pytest.fixture() +def remote_db(request): +    couch_url = request.config.option.couch_url + +    def create(uuid, create=True): +        db = UserDatabase(couch_url, uuid, create=create) +        request.addfinalizer(db.teardown) +        return db.setup() +    return create + + +def get_pid(pidfile): +    if not os.path.isfile(pidfile): +        return 0 +    try: +        with open(pidfile) as f: +            return int(f.read()) +    except IOError: +        return 0 + + +# +# soledad_server fixture: provides a running soledad server in a per module +# context (same soledad server for all tests in this module). +# + +class SoledadServer(object): + +    def __init__(self, tmpdir_factory, couch_url): +        tmpdir = tmpdir_factory.mktemp('soledad-server') +        self.tmpdir = tmpdir +        self._pidfile = os.path.join(tmpdir.strpath, 'soledad-server.pid') +        self._logfile = os.path.join(tmpdir.strpath, 'soledad-server.log') +        self._couch_url = couch_url + +    def start(self): +        self._create_conf_file() +        # start the server +        executable = 'twistd' +        if 'VIRTUAL_ENV' not in os.environ: +            executable = os.path.join( +                os.path.dirname(os.environ['_']), 'twistd') +        subprocess.check_call([ +            executable, +            '--logfile=%s' % self._logfile, +            '--pidfile=%s' % self._pidfile, +            'web', +            '--class=leap.soledad.server.entrypoints.SoledadEntrypoint', +            '--port=tcp:2424' +        ]) + +    def _create_conf_file(self): + +        # come up with name of the configuration file +        fname = '/etc/soledad/soledad-server.conf' +        if not os.access('/etc', os.W_OK): +            fname = os.path.join(self.tmpdir.strpath, 'soledad-server.conf') + +        # create the configuration file +        dirname = os.path.dirname(fname) +        if not os.path.isdir(dirname): +            os.mkdir(dirname) +        with open(fname, 'w') as f: +            blobs_path = os.path.join(str(self.tmpdir), 'blobs') +            content = '''[soledad-server] +couch_url = %s +blobs = true +blobs_path = %s''' % (self._couch_url, blobs_path) +            f.write(content) + +        # update the environment to use that file +        os.environ.update({'SOLEDAD_SERVER_CONFIG_FILE': fname}) + +    def stop(self): +        pid = get_pid(self._pidfile) +        os.kill(pid, signal.SIGTERM) + + +@pytest.fixture(scope='module') +def soledad_server(tmpdir_factory, request): + +    # avoid starting a server if the url is remote +    soledad_url = request.config.option.soledad_server_url +    if soledad_url is not None: +        return None + +    # start a soledad server +    couch_url = request.config.option.couch_url +    server = SoledadServer(tmpdir_factory, couch_url) +    server.start() +    request.addfinalizer(server.stop) +    return server + + +# +# soledad_dbs fixture: provides all databases needed by soledad server in a per +# module scope (same databases for all tests in this module). +# + +def _token_dbname(): +    dbname = 'tokens_' + \ +        str(int(time.time() / (30 * 24 * 3600))) +    return dbname + + +class SoledadDatabases(object): + +    def __init__(self, url, create=True): +        self._token_db_url = urljoin(url, _token_dbname()) +        self._shared_db_url = urljoin(url, 'shared') +        self._create = create + +    def setup(self, uuid): +        self._create_dbs() +        self._add_token(uuid) + +    def _create_dbs(self): +        _request('put', self._token_db_url, do=self._create) +        _request('put', self._shared_db_url, do=self._create) + +    def _add_token(self, uuid): +        token = sha512(DEFAULT_TOKEN).hexdigest() +        content = {'type': 'Token', 'user_id': uuid} +        _request('put', self._token_db_url + '/' + token, +                 data=json.dumps(content), do=self._create) + +    def teardown(self): +        _request('delete', self._token_db_url, do=self._create) +        _request('delete', self._shared_db_url, do=self._create) + + +@pytest.fixture() +def soledad_dbs(request): +    couch_url = request.config.option.couch_url + +    def create(uuid, create=True): +        db = SoledadDatabases(couch_url, create=create) +        request.addfinalizer(db.teardown) +        return db.setup(uuid) +    return create + + +# +# soledad_client fixture: provides a clean soledad client for a test function. +# + +def _get_certfile(url, tmpdir): + +    # download the certificate +    parsed = urlparse.urlsplit(url) +    netloc = re.sub('^[^\.]+\.', '', parsed.netloc) +    host, _ = netloc.split(':') +    response = requests.get('https://%s/ca.crt' % host, verify=False) + +    # store it in a temporary file +    cert_file = os.path.join(tmpdir.strpath, 'cert.pem') +    with open(cert_file, 'w') as f: +        f.write(response.text) + +    return cert_file + + +@pytest.fixture() +def soledad_client(tmpdir, soledad_server, remote_db, soledad_dbs, request): +    passphrase = DEFAULT_PASSPHRASE +    token = DEFAULT_TOKEN + +    # default values for local server +    server_url = DEFAULT_URL +    default_uuid = uuid4().hex +    create = True +    cert_file = None + +    # use values for remote server if server url is passed +    url_arg = request.config.option.soledad_server_url +    if url_arg: +        server_url = url_arg +        default_uuid = 'test-user' +        create = False +        cert_file = _get_certfile(server_url, tmpdir) + +    remote_db(default_uuid, create=create) +    soledad_dbs(default_uuid, create=create) + +    # get a soledad instance +    def create(force_fresh_db=False): +        secrets_file = '%s.secret' % default_uuid +        secrets_path = os.path.join(tmpdir.strpath, secrets_file) + +        # in some tests we might want to use the same user and remote database +        # but with a clean/empty local database (i.e. download benchmarks), so +        # here we provide a way to do that. +        idx = 1 +        if force_fresh_db: +            # find the next index for this user +            idx = len(glob.glob('%s/*-*.db' % tmpdir.strpath)) + 1 +        db_file = '%s-%d.db' % (default_uuid, idx) +        local_db_path = os.path.join(tmpdir.strpath, db_file) + +        soledad_client = Soledad( +            default_uuid, +            unicode(passphrase), +            secrets_path=secrets_path, +            local_db_path=local_db_path, +            server_url=server_url, +            cert_file=cert_file, +            auth_token=token, +            with_blobs=True) +        request.addfinalizer(soledad_client.close) +        return soledad_client +    return create + + +# +# pytest-benchmark customizations +# + +# avoid hooking if this is not a benchmarking environment +if 'pytest_benchmark' in sys.modules: + +    def pytest_benchmark_update_machine_info(config, machine_info): +        """ +        Add the host's hostname information to machine_info. + +        Get the value from the HOST_HOSTNAME environment variable if it is set, +        or from the actual system's hostname otherwise. +        """ +        hostname = os.environ.get('HOST_HOSTNAME', socket.gethostname()) +        machine_info['host'] = hostname + + +# +# benchmark/responsiveness fixtures +# + +@pytest.fixture() +def payload(): +    def generate(size): +        random.seed(1337)  # same seed to avoid different bench results +        payload_bytes = bytearray(random.getrandbits(8) for _ in xrange(size)) +        # encode as base64 to avoid ascii encode/decode errors +        return base64.b64encode(payload_bytes)[:size]  # remove b64 overhead +    return generate | 
