Skip to content

Commit

Permalink
#11616 disttrial worker test improvements (#11617)
Browse files Browse the repository at this point in the history
Some disttrial tests which previously depended on specific implementation details
of the local/worker protocol are now less dependent on those details.
  • Loading branch information
exarkun committed Sep 7, 2022
2 parents 0d9016c + 60c621d commit 2cb047b
Show file tree
Hide file tree
Showing 8 changed files with 483 additions and 201 deletions.
1 change: 1 addition & 0 deletions setup.cfg
Expand Up @@ -51,6 +51,7 @@ packages = find:
test =
cython-test-exception-raiser >= 1.0.2, <2
PyHamcrest >= 1.9.0
hypothesis ~= 6.0

; List of dependencies required to build the documentation and test the
; release scripts and process.
Expand Down
Empty file.
162 changes: 161 additions & 1 deletion src/twisted/trial/_dist/test/matchers.py
Expand Up @@ -5,8 +5,48 @@
Hamcrest matchers useful throughout the test suite.
"""

from hamcrest import equal_to, has_length, has_properties
__all__ = [
"matches_result",
"HasSum",
"IsSequenceOf",
]

from typing import 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_extensions 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)


def matches_result(
Expand All @@ -30,3 +70,123 @@ def matches_result(
"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]
)
188 changes: 188 additions & 0 deletions src/twisted/trial/_dist/test/test_matchers.py
@@ -0,0 +1,188 @@
"""
Tests for L{twisted.trial._dist.test.matchers}.
"""

from typing import Callable, Sequence, Tuple, Type

from hamcrest import anything, assert_that, contains, contains_string, equal_to, not_
from hamcrest.core.matcher import Matcher
from hamcrest.core.string_description import StringDescription
from hypothesis import given
from hypothesis.strategies import (
binary,
booleans,
integers,
just,
lists,
one_of,
sampled_from,
text,
tuples,
)

from twisted.python.failure import Failure
from twisted.trial.unittest import SynchronousTestCase
from .matchers import HasSum, IsSequenceOf, S, isFailure, similarFrame

Summer = Callable[[Sequence[S]], S]
concatInt = sum
concatStr = "".join
concatBytes = b"".join


class HasSumTests(SynchronousTestCase):
"""
Tests for L{HasSum}.
"""

summables = one_of(
tuples(lists(integers()), just(concatInt)),
tuples(lists(text()), just(concatStr)),
tuples(lists(binary()), just(concatBytes)),
)

@given(summables)
def test_matches(self, summable: Tuple[Sequence[S], Summer[S]]) -> None:
"""
L{HasSum} matches a sequence if the elements sum to a value matched by
the parameterized matcher.
:param summable: A tuple of a sequence of values to try to match and a
function which can compute the correct sum for that sequence.
"""
seq, sumFunc = summable
expected = sumFunc(seq)
zero = sumFunc([])
matcher = HasSum(equal_to(expected), zero)

description = StringDescription()
assert_that(matcher.matches(seq, description), equal_to(True))
assert_that(str(description), equal_to(""))

@given(summables)
def test_mismatches(
self,
summable: Tuple[
Sequence[S],
Summer[S],
],
) -> None:
"""
L{HasSum} does not match a sequence if the elements do not sum to a
value matched by the parameterized matcher.
:param summable: See L{test_matches}.
"""
seq, sumFunc = summable
zero = sumFunc([])
# A matcher that never matches.
sumMatcher: Matcher[S] = not_(anything())
matcher = HasSum(sumMatcher, zero)

actualDescription = StringDescription()
assert_that(matcher.matches(seq, actualDescription), equal_to(False))

sumMatcherDescription = StringDescription()
sumMatcherDescription.append_description_of(sumMatcher)
actualStr = str(actualDescription)
assert_that(actualStr, contains_string("a sequence with sum"))
assert_that(actualStr, contains_string(str(sumMatcherDescription)))


class IsSequenceOfTests(SynchronousTestCase):
"""
Tests for L{IsSequenceOf}.
"""

sequences = lists(booleans())

@given(integers(min_value=0, max_value=1000))
def test_matches(self, numItems: int) -> None:
"""
L{IsSequenceOf} matches a sequence if all of the elements are
matched by the parameterized matcher.
:param numItems: The length of a sequence to try to match.
"""
seq = [True] * numItems
matcher = IsSequenceOf(equal_to(True))

actualDescription = StringDescription()
assert_that(matcher.matches(seq, actualDescription), equal_to(True))
assert_that(str(actualDescription), equal_to(""))

@given(integers(min_value=0, max_value=1000), integers(min_value=0, max_value=1000))
def test_mismatches(self, numBefore: int, numAfter: int) -> None:
"""
L{IsSequenceOf} does not match a sequence if any of the elements
are not matched by the parameterized matcher.
:param numBefore: In the sequence to try to match, the number of
elements expected to match before an expected mismatch.
:param numAfter: In the sequence to try to match, the number of
elements expected expected to match after an expected mismatch.
"""
# Hide the non-matching value somewhere in the sequence.
seq = [True] * numBefore + [False] + [True] * numAfter
matcher = IsSequenceOf(equal_to(True))

actualDescription = StringDescription()
assert_that(matcher.matches(seq, actualDescription), equal_to(False))
actualStr = str(actualDescription)
assert_that(actualStr, contains_string("a sequence containing only"))
assert_that(
actualStr, contains_string(f"not sequence with element #{numBefore}")
)


class IsFailureTests(SynchronousTestCase):
"""
Tests for L{isFailure}.
"""

@given(sampled_from([ValueError, ZeroDivisionError, RuntimeError]))
def test_matches(self, excType: Type[BaseException]) -> None:
"""
L{isFailure} matches instances of L{Failure} with matching
attributes.
:param excType: An exception type to wrap in a L{Failure} to be
matched against.
"""
matcher = isFailure(type=equal_to(excType))
failure = Failure(excType())
assert_that(matcher.matches(failure), equal_to(True))

@given(sampled_from([ValueError, ZeroDivisionError, RuntimeError]))
def test_mismatches(self, excType: Type[BaseException]) -> None:
"""
L{isFailure} does not match instances of L{Failure} with
attributes that don't match.
:param excType: An exception type to wrap in a L{Failure} to be
matched against.
"""
matcher = isFailure(type=equal_to(excType), other=not_(anything()))
failure = Failure(excType())
assert_that(matcher.matches(failure), equal_to(False))

def test_frames(self):
"""
The L{similarFrame} matcher matches elements of the C{frames} list
of a L{Failure}.
"""
try:
raise ValueError("Oh no")
except BaseException:
f = Failure()

actualDescription = StringDescription()
matcher = isFailure(
frames=contains(similarFrame("test_frames", "test_matchers"))
)
assert_that(
matcher.matches(f, actualDescription),
equal_to(True),
actualDescription,
)

0 comments on commit 2cb047b

Please sign in to comment.