Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#11616 disttrial worker test improvements #11617

Merged
merged 38 commits into from Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
81e94e8
add HasSum, IsSequenceOf, isFailure, similarFrame, and isTuple matchers
exarkun Aug 24, 2022
f6c57c2
Tests for isTuple
exarkun Aug 25, 2022
0c9f6a2
Tests for isFailure and some bug fixes
exarkun Aug 25, 2022
f004be9
Fix WorkerProtocolTests and LocalWorkerAMPTests
exarkun Aug 25, 2022
ed56326
news fragment
exarkun Aug 25, 2022
1f60522
use typing.List to support more Python versions
exarkun Aug 26, 2022
d949c89
switch to typing_extensions.Protocol to support more Python versions
exarkun Aug 26, 2022
dc28cb8
Merge remote-tracking branch 'origin/trunk' into 11616-test_worker-im…
exarkun Aug 26, 2022
e1eac51
some changes which hopefully improve readability
exarkun Aug 26, 2022
e51fb63
Side-step pyhamcrest's nonsensical types
exarkun Aug 26, 2022
9eab861
restore more of the original intent of this test
exarkun Aug 26, 2022
6deb9f1
flatten the matcher in test_runError to see if it helps readability
exarkun Aug 26, 2022
53471a6
Flatten the nested matches into several simpler matches
exarkun Aug 29, 2022
6412a67
remove unused imports
exarkun Aug 29, 2022
aff5118
simplify one more hamcrest usage
exarkun Aug 29, 2022
798952f
type annotate `makeTodo`
exarkun Aug 29, 2022
a45afc4
Force the correct types for trial's TestResult
exarkun Aug 29, 2022
416afd0
Old Python compat
exarkun Aug 29, 2022
9502b66
Merge remote-tracking branch 'origin/trunk' into 11616-test_worker-im…
exarkun Sep 1, 2022
a78ee19
Turn on remote debugging
exarkun Sep 1, 2022
057c67e
do tmate first
exarkun Sep 1, 2022
05077e2
some debug junk
exarkun Sep 1, 2022
0af8813
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 1, 2022
495a662
don't twiddle stdout, it makes debugging hard
exarkun Sep 1, 2022
3ca8770
Merge remote-tracking branch 'mine/11616-test_worker-improvements' in…
exarkun Sep 1, 2022
dd1482c
breaks other tests, hampers reproduction
exarkun Sep 1, 2022
2ab4d64
better failure messages
exarkun Sep 1, 2022
d4b5d21
let tests run first so tox initializes the environment, but end the t…
exarkun Sep 1, 2022
e0f8742
dump more debug info
exarkun Sep 1, 2022
3a3f278
not really usable
exarkun Sep 1, 2022
7a2775d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 1, 2022
b00c683
maybe eek out a bit more info
exarkun Sep 6, 2022
a52d00c
Merge remote-tracking branch 'mine/11616-test_worker-improvements' in…
exarkun Sep 6, 2022
804ceef
debug stuff interferes with test logic! rad
exarkun Sep 6, 2022
d612581
remove debug stuff
exarkun Sep 6, 2022
78deb9f
Merge remote-tracking branch 'origin/trunk' into 11616-test_worker-im…
exarkun Sep 6, 2022
a148193
Merge branch '11638.manhole-vs-excepthook' into 11616-test_worker-imp…
exarkun Sep 6, 2022
60c621d
Merge branch 'trunk' into 11616-test_worker-improvements
exarkun Sep 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
)