# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Hamcrest matchers useful throughout the test suite.
"""
__all__ = [
"matches_result",
"HasSum",
"IsSequenceOf",
]
from typing import Any, List, Sequence, Tuple, TypeVar
from hamcrest import (
contains_exactly,
contains_string,
equal_to,
has_length,
has_properties,
instance_of,
)
from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.core.allof import AllOf
from hamcrest.core.description import Description
from hamcrest.core.matcher import Matcher
from typing import Protocol
from twisted.python.failure import Failure
T = TypeVar("T")
class Semigroup(Protocol[T]):
"""
A type with an associative binary operator.
Common examples of a semigroup are integers with addition and strings with
concatenation.
"""
def __add__(self, other: T) -> T:
"""
This must be associative: a + (b + c) == (a + b) + c
"""
S = TypeVar("S", bound=Semigroup[Any])
def matches_result(
successes: Matcher[Any] = equal_to(0),
errors: Matcher[Any] = has_length(0),
failures: Matcher[Any] = has_length(0),
skips: Matcher[Any] = has_length(0),
expectedFailures: Matcher[Any] = has_length(0),
unexpectedSuccesses: Matcher[Any] = has_length(0),
) -> Matcher[Any]:
"""
Match a L{TestCase} instances with matching attributes.
"""
return has_properties(
{
"successes": successes,
"errors": errors,
"failures": failures,
"skips": skips,
"expectedFailures": expectedFailures,
"unexpectedSuccesses": unexpectedSuccesses,
}
)
class HasSum(BaseMatcher[Sequence[S]]):
"""
Match a sequence the elements of which sum to a value matched by
another matcher.
:ivar sumMatcher: The matcher which must match the sum.
:ivar zero: The zero value for the matched type.
"""
def __init__(self, sumMatcher: Matcher[S], zero: S) -> None:
self.sumMatcher = sumMatcher
self.zero = zero
def _sum(self, sequence: Sequence[S]) -> S:
if not sequence:
return self.zero
result = self.zero
for elem in sequence:
result = result + elem
return result
def _matches(self, item: Sequence[S]) -> bool:
"""
Determine whether the sum of the sequence is matched.
"""
s = self._sum(item)
return self.sumMatcher.matches(s)
def describe_mismatch(self, item: Sequence[S], description: Description) -> None:
"""
Describe the mismatch.
"""
s = self._sum(item)
description.append_description_of(self)
self.sumMatcher.describe_mismatch(s, description)
return None
def describe_to(self, description: Description) -> None:
"""
Describe this matcher for error messages.
"""
description.append_text("a sequence with sum ")
description.append_description_of(self.sumMatcher)
description.append_text(", ")
class IsSequenceOf(BaseMatcher[Sequence[T]]):
"""
Match a sequence where every element is matched by another matcher.
:ivar elementMatcher: The matcher which must match every element of the
sequence.
"""
def __init__(self, elementMatcher: Matcher[T]) -> None:
self.elementMatcher = elementMatcher
def _matches(self, item: Sequence[T]) -> bool:
"""
Determine whether every element of the sequence is matched.
"""
for elem in item:
if not self.elementMatcher.matches(elem):
return False
return True
def describe_mismatch(self, item: Sequence[T], description: Description) -> None:
"""
Describe the mismatch.
"""
for idx, elem in enumerate(item):
if not self.elementMatcher.matches(elem):
description.append_description_of(self)
description.append_text(f"not sequence with element #{idx} {elem!r}")
def describe_to(self, description: Description) -> None:
"""
Describe this matcher for error messages.
"""
description.append_text("a sequence containing only ")
description.append_description_of(self.elementMatcher)
description.append_text(", ")
def isFailure(**properties: Matcher[object]) -> Matcher[object]:
"""
Match an instance of L{Failure} with matching attributes.
"""
return AllOf(
instance_of(Failure),
has_properties(**properties),
)
def similarFrame(
functionName: str, fileName: str
) -> Matcher[Sequence[Tuple[str, str, int, List[object], List[object]]]]:
"""
Match a tuple representation of a frame like those used by
L{twisted.python.failure.Failure}.
"""
# The frames depend on exact layout of the source
# code in files and on the filesystem so we won't
# bother being very precise here. Just verify we
# see some distinctive fragments.
#
# In particular, the last frame should be a tuple like
#
# (functionName, fileName, someint, [], [])
return contains_exactly(
equal_to(functionName),
contains_string(fileName), # type: ignore[arg-type]
instance_of(int), # type: ignore[arg-type]
# Unfortunately Failure makes them sometimes tuples, sometimes
# dict_items.
has_length(0), # type: ignore[arg-type]
has_length(0), # type: ignore[arg-type]
)
|