Skip to content
Snippets Groups Projects
matchers.py 5.32 KiB
Newer Older
# Copyright 2019 PrivateStorage.io, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Testtools matchers useful for the test suite.
"""

__all__ = [
    "Provides",
    "raises",
    "returns",
    "matches_version_dictionary",
    "between",
    "leases_current",
]

Tom Prince's avatar
Tom Prince committed
from datetime import datetime
import attr
from testtools.matchers import (
Tom Prince's avatar
Tom Prince committed
    AfterPreprocessing,
    AllMatch,
    Always,
Tom Prince's avatar
Tom Prince committed
    ContainsDict,
    Equals,
    GreaterThan,
    LessThan,
    Matcher,
Tom Prince's avatar
Tom Prince committed
    Mismatch,
Tom Prince's avatar
Tom Prince committed
from testtools.twistedsupport import succeeded
from treq import content
Tom Prince's avatar
Tom Prince committed
from ._exception import raises
Tom Prince's avatar
Tom Prince committed

@attr.s
class Provides(object):
    """
    Match objects that provide all of a list of Zope Interface interfaces.
Tom Prince's avatar
Tom Prince committed

    interfaces = attr.ib(validator=attr.validators.instance_of(list))

    def match(self, obj):
        missing = set()
        for iface in self.interfaces:
            if not iface.providedBy(obj):
                missing.add(iface)
        if missing:
Tom Prince's avatar
Tom Prince committed
            return Mismatch(
                "{} does not provide expected {}".format(
                    obj,
                    ", ".join(str(iface) for iface in missing),
                )
            )


def matches_version_dictionary():
    """
    Match the dictionary returned by Tahoe-LAFS'
    ``RIStorageServer.get_version`` which is also the dictionary returned by
    our own ``RIPrivacyPassAuthorizedStorageServer.get_version``.
Tom Prince's avatar
Tom Prince committed
    return ContainsDict(
        {
            # It has these two top-level keys, at least.  Try not to be too
            # fragile by asserting much more than that they are present.
            b"application-version": Always(),
            b"http://allmydata.org/tahoe/protocols/storage/v1": Always(),
        }
    )


def returns(matcher):
    """
    Matches a no-argument callable that returns a value matched by the given
    matcher.
    """
    return _Returns(matcher)


class _Returns(Matcher):
    def __init__(self, result_matcher):
        self.result_matcher = result_matcher

    def match(self, matchee):
        return self.result_matcher.match(matchee())

    def __str__(self):
        return "Returns({})".format(self.result_matcher)
def greater_or_equal(v):
    """
    Matches a value greater than or equal to ``v``.
    """
    return MatchesAny(GreaterThan(v), Equals(v))


def lesser_or_equal(v):
    """
    Matches a value less than or equal to ``v``.
    """
    return MatchesAny(LessThan(v), Equals(v))


def between(low, high):
    """
    Matches a value in the range [low, high].
    """
    return MatchesAll(
        greater_or_equal(low),
        lesser_or_equal(high),


def leases_current(relevant_storage_indexes, now, min_lease_remaining):
    """
    Return a matcher on a ``DummyStorageServer`` instance which matches
    servers for which the leases on the given storage indexes do not expire
    before ``min_lease_remaining``.
    """
Jean-Paul Calderone's avatar
Jean-Paul Calderone committed

    def get_relevant_stats(storage_server):
        for (storage_index, shares) in storage_server.buckets.items():
            if storage_index in relevant_storage_indexes:
                for (sharenum, stat) in shares.items():
                    yield stat

    return AfterPreprocessing(
        # Get share stats for storage indexes we should have
        # visited and maintained.
        lambda storage_server: list(get_relevant_stats(storage_server)),
        AllMatch(
            AfterPreprocessing(
                # Lease expiration for anything visited must be
                # further in the future than min_lease_remaining,
                # either because it had time left or because we
                # renewed it.
Tom Prince's avatar
Tom Prince committed
                lambda share_stat: datetime.utcfromtimestamp(
                    share_stat.lease_expiration
                ),


def even():
    """
    Matches even integers.
    """
    return AfterPreprocessing(
        lambda n: n % 2,
        Equals(0),
    )


def odd():
    """
    Matches odd integers.
    """
    return AfterPreprocessing(
        lambda n: n % 2,
        Equals(1),
    )
Tom Prince's avatar
Tom Prince committed
def matches_response(
    code_matcher=Always(), headers_matcher=Always(), body_matcher=Always()
):
    """
    Match a Treq response object with certain code and body.

    :param Matcher code_matcher: A matcher to apply to the response code.

    :param Matcher headers_matcher: A matcher to apply to the response headers
        (a ``twisted.web.http_headers.Headers`` instance).

    :param Matcher body_matcher: A matcher to apply to the response body.

    :return: A matcher.
    """
    return MatchesAll(
        MatchesStructure(
            code=code_matcher,
            headers=headers_matcher,
        ),
        AfterPreprocessing(
            lambda response: content(response),
            succeeded(body_matcher),
        ),
    )