diff --git a/.gitignore b/.gitignore index 9f48daf8..4702ad0a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ *~ .DS_Store /.eggs -/.hypothesis/ /.tox/ /apidocs/ /build/ diff --git a/mypy.ini b/mypy.ini index 1d53011e..94e2a3ff 100644 --- a/mypy.ini +++ b/mypy.ini @@ -63,11 +63,6 @@ ignore_missing_imports = True [mypy-treq.*] ignore_missing_imports = True -[mypy-hypothesis] -ignore_missing_imports = True -[mypy-hypothesis.*] -ignore_missing_imports = True - [mypy-idna] ignore_missing_imports = True diff --git a/requirements/tox-tests.txt b/requirements/tox-tests.txt index 5dbccbaf..d17b2197 100644 --- a/requirements/tox-tests.txt +++ b/requirements/tox-tests.txt @@ -1,3 +1,2 @@ treq==22.2.0 -hypothesis==6.48.2 idna==3.3 diff --git a/src/klein/test/__init__.py b/src/klein/test/__init__.py index 537416ef..e78e3494 100644 --- a/src/klein/test/__init__.py +++ b/src/klein/test/__init__.py @@ -4,15 +4,3 @@ """ Tests for L{klein}. """ - -from hypothesis import HealthCheck, settings - - -settings.register_profile( - "patience", - settings( - deadline=None, - suppress_health_check=[HealthCheck.too_slow], - ), -) -settings.load_profile("patience") diff --git a/src/klein/test/not_hypothesis.py b/src/klein/test/not_hypothesis.py new file mode 100644 index 00000000..da8f6142 --- /dev/null +++ b/src/klein/test/not_hypothesis.py @@ -0,0 +1,175 @@ +""" +We have had a history of U{bad +experiences} with Hypothesis in +Klein, and maybe it's not actually a good application of this tool at all. As +such we have removed it, at least for now. This module presents a vaguely +Hypothesis-like stub, to keep the structure of our tests in a +Hypothesis-friendly shape, in case we want to put it back. +""" + +from functools import wraps +from itertools import product +from string import ascii_uppercase +from typing import Callable, Iterable, Optional, Tuple, TypeVar + +from hyperlink import DecodedURL + + +T = TypeVar("T") +S = TypeVar("S") + + +def given( + *args: Callable[[], Iterable[T]], + **kwargs: Callable[[], Iterable[T]], +) -> Callable[[Callable[..., None]], Callable[..., None]]: + def decorator(testMethod: Callable[..., None]) -> Callable[..., None]: + @wraps(testMethod) + def realTestMethod(self: S) -> None: + everyPossibleArgs = product( + *[eachFactory() for eachFactory in args] + ) + everyPossibleKwargs = product( + *[ + [(name, eachValue) for eachValue in eachFactory()] + for (name, eachFactory) in kwargs.items() + ] + ) + everyPossibleSignature = product( + everyPossibleArgs, everyPossibleKwargs + ) + # not quite the _full_ cartesian product but the whole point is + # that we're making a feeble attempt at this rather than bringing + # in hypothesis. + for computedArgs, computedPairs in everyPossibleSignature: + computedKwargs = dict(computedPairs) + testMethod(self, *computedArgs, **computedKwargs) + + return realTestMethod + + return decorator + + +def binary() -> Callable[[], Iterable[bytes]]: + """ + Generate some binary data. + """ + + def params() -> Iterable[bytes]: + return [b"data", b"data data data", b"\x00" * 50, b""] + + return params + + +def ascii_text(min_size: int) -> Callable[[], Iterable[str]]: + """ + Generate some ASCII strs. + """ + + def params() -> Iterable[str]: + yield from [ + "ascii-text", + "some more ascii text", + ] + assert min_size, "nothing needs 0-length strings right now" + + return params + + +def latin1_text(min_size: int = 0) -> Callable[[], Iterable[str]]: + """ + Generate some strings encodable as latin1 + """ + + def params() -> Iterable[str]: + yield from [ + "latin1-text", + "some more latin1 text", + "hére is latin1 text", + ] + if not min_size: + yield "" + + return params + + +def text( + min_size: int = 0, alphabet: Optional[str] = None +) -> Callable[[], Iterable[str]]: + """ + Generate some text. + """ + + def params() -> Iterable[str]: + if alphabet == ascii_uppercase: + yield from ascii_text(min_size)() + return + yield from latin1_text(min_size)() + yield "\N{SNOWMAN}" + + return params + + +def textHeaderPairs() -> Callable[[], Iterable[Iterable[Tuple[str, str]]]]: + """ + Generate some pairs of headers with text values. + """ + + def params() -> Iterable[Iterable[Tuple[str, str]]]: + return [[], [("text", "header")]] + + return params + + +def bytesHeaderPairs() -> Callable[[], Iterable[Iterable[Tuple[str, bytes]]]]: + """ + Generate some pairs of headers with bytes values. + """ + + def params() -> Iterable[Iterable[Tuple[str, bytes]]]: + return [[], [("bytes", b"header")]] + + return params + + +def booleans() -> Callable[[], Iterable[bool]]: + def parameters() -> Iterable[bool]: + yield True + yield False + + return parameters + + +def jsonObjects() -> Callable[[], Iterable[object]]: + def parameters() -> Iterable[object]: + yield {} + yield {"hello": "world"} + yield {"here is": {"some": "nesting"}} + yield { + "and": "multiple", + "keys": { + "with": "nesting", + "and": 1234, + "numbers": ["with", "lists", "too"], + "also": ("tuples", "can", "serialize"), + }, + } + + return parameters + + +def decoded_urls() -> Callable[[], Iterable[DecodedURL]]: + """ + Generate a few URLs U{with only path and domain names + } kind of like + Hyperlink's own hypothesis strategy. + """ + + def parameters() -> Iterable[DecodedURL]: + yield DecodedURL.from_text("https://example.com/") + yield DecodedURL.from_text("https://example.com") + yield DecodedURL.from_text("http://example.com/") + yield DecodedURL.from_text("https://example.com/é") + yield DecodedURL.from_text("https://súbdomain.example.com/ascii/path/") + + return parameters diff --git a/src/klein/test/test_headers.py b/src/klein/test/test_headers.py index bb236279..002281a4 100644 --- a/src/klein/test/test_headers.py +++ b/src/klein/test/test_headers.py @@ -7,7 +7,6 @@ from abc import ABC, abstractmethod from collections import defaultdict -from string import ascii_letters from typing import ( AnyStr, Callable, @@ -20,17 +19,6 @@ cast, ) -from hypothesis import given -from hypothesis.strategies import ( - binary, - characters, - composite, - iterables, - lists, - text, - tuples, -) - from .._headers import ( HEADER_NAME_ENCODING, HEADER_VALUE_ENCODING, @@ -49,6 +37,14 @@ normalizeRawHeadersFrozen, ) from ._trial import TestCase +from .not_hypothesis import ( + binary, + bytesHeaderPairs, + given, + latin1_text, + text, + textHeaderPairs, +) __all__ = () @@ -58,55 +54,6 @@ DrawCallable = Callable[[Callable[..., T]], T] -@composite -def ascii_text( - draw: DrawCallable, - min_size: Optional[int] = 0, - max_size: Optional[int] = None, -) -> str: # pragma: no cover - """ - A strategy which generates ASCII-encodable text. - - @param min_size: The minimum number of characters in the text. - C{None} is treated as C{0}. - - @param max_size: The maximum number of characters in the text. - Use C{None} for an unbounded size. - """ - return cast( - str, - draw( - text(min_size=min_size, max_size=max_size, alphabet=ascii_letters) - ), - ) - - -@composite # pragma: no cover -def latin1_text( - draw: DrawCallable, - min_size: Optional[int] = 0, - max_size: Optional[int] = None, -) -> str: - """ - A strategy which generates ISO-8859-1-encodable text. - - @param min_size: The minimum number of characters in the text. - C{None} is treated as C{0}. - - @param max_size: The maximum number of characters in the text. - Use C{None} for an unbounded size. - """ - return "".join( - draw( - lists( - characters(max_codepoint=255), - min_size=min_size, - max_size=max_size, - ) - ) - ) - - def encodeName(name: str) -> Optional[bytes]: return name.encode(HEADER_NAME_ENCODING) @@ -304,7 +251,7 @@ def headerNormalize(self, value: str) -> str: """ return value - @given(iterables(tuples(ascii_text(min_size=1), latin1_text()))) + @given(textHeaderPairs()) def test_getTextName(self, textPairs: Iterable[Tuple[str, str]]) -> None: """ C{getValues} returns an iterable of L{str} values for @@ -333,7 +280,7 @@ def test_getTextName(self, textPairs: Iterable[Tuple[str, str]]) -> None: f"header name: {name!r}", ) - @given(iterables(tuples(ascii_text(min_size=1), binary()))) + @given(bytesHeaderPairs()) def test_getTextNameBinaryValues( self, pairs: Iterable[Tuple[str, bytes]] ) -> None: diff --git a/src/klein/test/test_message.py b/src/klein/test/test_message.py index f07dfa4d..fd8d0c35 100644 --- a/src/klein/test/test_message.py +++ b/src/klein/test/test_message.py @@ -8,14 +8,12 @@ from abc import ABC, abstractmethod from typing import cast -from hypothesis import given -from hypothesis.strategies import binary - from twisted.internet.defer import ensureDeferred from .._imessage import IHTTPMessage from .._message import FountAlreadyAccessedError, bytesToFount, fountToBytes from ._trial import TestCase +from .not_hypothesis import binary, given __all__ = () diff --git a/src/klein/test/test_plating.py b/src/klein/test/test_plating.py index c11f2fc2..5cf795bb 100644 --- a/src/klein/test/test_plating.py +++ b/src/klein/test/test_plating.py @@ -3,14 +3,10 @@ """ -import atexit import json -from string import printable from typing import Any import attr -from hypothesis import given, settings -from hypothesis import strategies as st from twisted.internet.defer import Deferred, succeed from twisted.trial.unittest import SynchronousTestCase @@ -20,6 +16,7 @@ from .. import Klein, Plating from .._plating import ATOM_TYPES, PlatedElement, resolveDeferredObjects +from .not_hypothesis import booleans, given, jsonObjects from .test_resource import MockRequest, _render @@ -119,46 +116,6 @@ def resolve(self): self.deferred.callback(self.value) -jsonAtoms = ( - st.none() - | st.booleans() - | st.integers() - | st.floats(allow_nan=False) - | st.text(printable) -) - - -def jsonComposites(children): - """ - Creates a Hypothesis strategy that constructs composite - JSON-serializable objects (e.g., lists). - - @param children: A strategy from which each composite object's - children will be drawn. - - @return: The composite objects strategy. - """ - return ( - st.lists(children) - | st.dictionaries(st.text(printable), children) - | st.tuples(children) - ) - - -jsonObjects = st.recursive(jsonAtoms, jsonComposites, max_leaves=200) - - -@atexit.register -def invalidateJsonStrategy() -> None: - """ - hypothesis RecursiveStrategy hangs on to a threadlocal object which causes - disttrial to hang for some reason. - - Possibly related to U{this }. - """ - jsonObjects.limited_base._threadlocal = None - - def transformJSONObject(jsonObject, transformer): """ Recursively apply a transforming function to a JSON serializable @@ -252,22 +209,22 @@ class ResolveDeferredObjectsTests(SynchronousTestCase): Tests for L{resolve_deferred_objects}. """ - @settings(max_examples=500) @given( - jsonObject=jsonObjects, - data=st.data(), + jsonObject=jsonObjects(), + shouldWrapDeferred=booleans(), ) - def test_resolveObjects(self, jsonObject, data): + def test_resolveObjects( + self, jsonObject: object, shouldWrapDeferred: bool + ) -> None: """ A JSON serializable object that may contain L{Deferred}s or a L{Deferred} that resolves to a JSON serializable object resolves to an object that contains no L{Deferred}s. """ deferredValues = [] - choose = st.booleans() def maybeWrapInDeferred(value): - if data.draw(choose): + if shouldWrapDeferred: deferredValues.append(DeferredValue(value)) return deferredValues[-1].deferred else: @@ -286,18 +243,19 @@ def maybeWrapInDeferred(value): self.assertEqual(self.successResultOf(resolved), jsonObject) @given( - jsonObject=jsonObjects, - data=st.data(), + jsonObject=jsonObjects(), + shouldInjectElements=booleans(), ) - def test_elementSerialized(self, jsonObject, data): + def test_elementSerialized( + self, jsonObject: object, shouldInjectElements: bool + ) -> None: """ A L{PlatedElement} within a JSON serializable object replaced by its JSON representation. """ - choose = st.booleans() def injectPlatingElements(value): - if data.draw(choose) and isinstance(value, dict): + if shouldInjectElements and isinstance(value, dict): return PlatedElement( slot_data=value, preloaded=tags.html(), diff --git a/src/klein/test/test_request_compat.py b/src/klein/test/test_request_compat.py index 5096bbc6..3a40d2c5 100644 --- a/src/klein/test/test_request_compat.py +++ b/src/klein/test/test_request_compat.py @@ -19,9 +19,6 @@ ) from hyperlink import DecodedURL, EncodedURL -from hyperlink.hypothesis import decoded_urls -from hypothesis import given -from hypothesis.strategies import binary, text from twisted.internet.defer import ensureDeferred from twisted.web.iweb import IRequest @@ -31,6 +28,7 @@ from .._request import IHTTPRequest from .._request_compat import HTTPRequestWrappingIRequest from ._trial import TestCase +from .not_hypothesis import binary, decoded_urls, given, text from .test_resource import MockRequest diff --git a/tox.ini b/tox.ini index 389ea652..441b87b4 100644 --- a/tox.ini +++ b/tox.ini @@ -67,7 +67,6 @@ setenv = coverage: COVERAGE_PROCESS_START={toxinidir}/.coveragerc TRIAL_JOBS={env:TRIAL_JOBS:--jobs=2} - HYPOTHESIS_STORAGE_DIRECTORY={toxworkdir}/hypothesis commands = # Run trial without coverage