From ba2c6f7f7fe69a356e8da9e64d2a20ce15321b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sviatoslav=20Sydorenko=20=28=D0=A1=D0=B2=D1=8F=D1=82=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1=D0=B8=D0=B4=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE=29?= Date: Sun, 6 Oct 2024 00:48:20 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20Integrate=20Hypothesis=20in=20te?= =?UTF-8?q?sts=20(#860)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 4 ++ CHANGES/860.contrib.rst | 3 ++ requirements/test.txt | 1 + tests/test_quoting.py | 89 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 CHANGES/860.contrib.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 64e9e3587..5d04910e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -113,6 +113,7 @@ repos: alias: mypy-py313 name: MyPy, for Python 3.13 additional_dependencies: + - hypothesis - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report` - multidict - pytest @@ -128,6 +129,7 @@ repos: alias: mypy-py312 name: MyPy, for Python 3.12 additional_dependencies: + - hypothesis - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report` - multidict - pytest @@ -143,6 +145,7 @@ repos: alias: mypy-py310 name: MyPy, for Python 3.10 additional_dependencies: + - hypothesis - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report` - multidict - pytest @@ -160,6 +163,7 @@ repos: alias: mypy-py38 name: MyPy, for Python 3.8 additional_dependencies: + - hypothesis - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report` - multidict - pytest diff --git a/CHANGES/860.contrib.rst b/CHANGES/860.contrib.rst new file mode 100644 index 000000000..99ebba6ba --- /dev/null +++ b/CHANGES/860.contrib.rst @@ -0,0 +1,3 @@ +Started testing with Hypothesis -- by :user:`webknjaz` and :user:`bdraco`. + +Special thanks to :user:`Zac-HD` for helping us get started with this framework. diff --git a/requirements/test.txt b/requirements/test.txt index f23edfc49..6957cd878 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,5 +1,6 @@ -r cython.txt covdefaults +hypothesis>=6.0 idna==3.10 multidict==6.1.0 pytest==8.3.3 diff --git a/tests/test_quoting.py b/tests/test_quoting.py index d9b6ae8e4..1f9cba6aa 100644 --- a/tests/test_quoting.py +++ b/tests/test_quoting.py @@ -1,4 +1,8 @@ +from typing import Type + import pytest +from hypothesis import assume, example, given, note +from hypothesis import strategies as st from yarl._quoting import NO_EXTENSIONS from yarl._quoting_py import _Quoter as _PyQuoter @@ -16,6 +20,10 @@ def quoter(request): def unquoter(request): return request.param + quoters = [_PyQuoter, _CQuoter] + quoter_ids = ["PyQuoter", "CQuoter"] + unquoters = [_PyUnquoter, _CUnquoter] + unquoter_ids = ["PyUnquoter", "CUnquoter"] else: @pytest.fixture(params=[_PyQuoter], ids=["py_quoter"]) @@ -26,6 +34,11 @@ def quoter(request): def unquoter(request): return request.param + quoters = [_PyQuoter] + quoter_ids = ["PyQuoter"] + unquoters = [_PyUnquoter] + unquoter_ids = ["PyUnquoter"] + def hexescape(char): """Escape char as RFC 2396 specifies""" @@ -455,3 +468,79 @@ def test_quoter_path_with_plus(quoter): def test_unquoter_path_with_plus(unquoter): s = "/test/x+y%2Bz/:+%2B/" assert "/test/x+y+z/:++/" == unquoter(unsafe="+")(s) + + +@given(safe=st.text(), protected=st.text(), qs=st.booleans(), requote=st.booleans()) +def test_fuzz__PyQuoter(safe, protected, qs, requote): + """Verify that _PyQuoter can be instantiated with any valid arguments.""" + assert _PyQuoter(safe=safe, protected=protected, qs=qs, requote=requote) + + +@given(ignore=st.text(), unsafe=st.text(), qs=st.booleans()) +def test_fuzz__PyUnquoter(ignore, unsafe, qs): + """Verify that _PyUnquoter can be instantiated with any valid arguments.""" + assert _PyUnquoter(ignore=ignore, unsafe=unsafe, qs=qs) + + +@example(text_input="0") +@given( + text_input=st.text( + alphabet=st.characters(max_codepoint=127, blacklist_characters="%") + ), +) +@pytest.mark.parametrize("quoter", quoters, ids=quoter_ids) +@pytest.mark.parametrize("unquoter", unquoters, ids=unquoter_ids) +def test_quote_unquote_parameter( + quoter: Type[_PyQuoter], + unquoter: Type[_PyUnquoter], + text_input: str, +) -> None: + quote = quoter() + unquote = unquoter() + text_quoted = quote(text_input) + note(f"text_quoted={text_quoted!r}") + text_output = unquote(text_quoted) + assert text_input == text_output + + +@example(text_input="0") +@given( + text_input=st.text( + alphabet=st.characters(max_codepoint=127, blacklist_characters="%") + ), +) +@pytest.mark.parametrize("quoter", quoters, ids=quoter_ids) +@pytest.mark.parametrize("unquoter", unquoters, ids=unquoter_ids) +def test_quote_unquote_parameter_requote( + quoter: Type[_PyQuoter], + unquoter: Type[_PyUnquoter], + text_input: str, +) -> None: + quote = quoter(requote=True) + unquote = unquoter() + text_quoted = quote(text_input) + note(f"text_quoted={text_quoted!r}") + text_output = unquote(text_quoted) + assert text_input == text_output + + +@example(text_input="0") +@given( + text_input=st.text( + alphabet=st.characters(max_codepoint=127, blacklist_characters="%") + ), +) +@pytest.mark.parametrize("quoter", quoters, ids=quoter_ids) +@pytest.mark.parametrize("unquoter", unquoters, ids=unquoter_ids) +def test_quote_unquote_parameter_path_safe( + quoter: Type[_PyQuoter], + unquoter: Type[_PyUnquoter], + text_input: str, +) -> None: + quote = quoter() + unquote = unquoter(ignore="/%", unsafe="+") + assume("+" not in text_input and "/" not in text_input) + text_quoted = quote(text_input) + note(f"text_quoted={text_quoted!r}") + text_output = unquote(text_quoted) + assert text_input == text_output