Skip to content

Commit

Permalink
Merge pull request #2995 from Zalathar/url-fragments-coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD authored May 30, 2021
2 parents 126ce20 + 03085f6 commit b12dbe6
Show file tree
Hide file tree
Showing 14 changed files with 74 additions and 37 deletions.
6 changes: 6 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
RELEASE_TYPE: patch

This release adjusts some internal code to help make our test suite more
reliable.

There is no user-visible change.
2 changes: 1 addition & 1 deletion hypothesis-python/src/hypothesis/extra/django/_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def _for_text(field):
# Not maximally efficient, but it makes pathological cases rarer.
# If you want a challenge: extend https://qntm.org/greenery to
# compute intersections of the full Python regex language.
return st.one_of(*[st.from_regex(r) for r in regexes])
return st.one_of(*(st.from_regex(r) for r in regexes))
# If there are no (usable) regexes, we use a standard text strategy.
min_size, max_size = length_bounds_from_validators(field)
strategy = st.text(
Expand Down
2 changes: 1 addition & 1 deletion hypothesis-python/src/hypothesis/extra/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1489,5 +1489,5 @@ def array_for(index_shape, size):
)

return result_shape.flatmap(
lambda index_shape: st.tuples(*[array_for(index_shape, size) for size in shape])
lambda index_shape: st.tuples(*(array_for(index_shape, size) for size in shape))
)
2 changes: 1 addition & 1 deletion hypothesis-python/src/hypothesis/internal/filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def numeric_bounds_from_ast(

if isinstance(tree, ast.BoolOp) and isinstance(tree.op, ast.And):
return merge_preds(
*[numeric_bounds_from_ast(node, argname, fallback) for node in tree.values]
*(numeric_bounds_from_ast(node, argname, fallback) for node in tree.values)
)

return fallback
Expand Down
37 changes: 21 additions & 16 deletions hypothesis-python/src/hypothesis/provisional.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def do_draw(self, data):
.filter(lambda tld: len(tld) + 2 <= self.max_length)
.flatmap(
lambda tld: st.tuples(
*[st.sampled_from([c.lower(), c.upper()]) for c in tld]
*(st.sampled_from([c.lower(), c.upper()]) for c in tld)
).map("".join)
)
)
Expand Down Expand Up @@ -142,6 +142,25 @@ def domains(
)


# The `urls()` strategy uses this to generate URL fragments (e.g. "#foo").
# It has been extracted to top-level so that we can test it independently
# of `urls()`, which helps with getting non-flaky coverage of the lambda.
_url_fragments_strategy = (
st.lists(
st.builds(
lambda char, encode: f"%{ord(char):02X}"
if (encode or char not in FRAGMENT_SAFE_CHARACTERS)
else char,
st.characters(min_codepoint=0, max_codepoint=255),
st.booleans(),
),
min_size=1,
)
.map("".join)
.map("#{}".format)
)


@defines_strategy(force_reusable_values=True)
def urls() -> st.SearchStrategy[str]:
"""A strategy for :rfc:`3986`, generating http/https URLs."""
Expand All @@ -152,26 +171,12 @@ def url_encode(s):
schemes = st.sampled_from(["http", "https"])
ports = st.integers(min_value=0, max_value=2 ** 16 - 1).map(":{}".format)
paths = st.lists(st.text(string.printable).map(url_encode)).map("/".join)
fragments = (
st.lists(
st.builds(
lambda char, encode: f"%{ord(char):02X}"
if (encode or char not in FRAGMENT_SAFE_CHARACTERS)
else char,
st.characters(min_codepoint=0, max_codepoint=255),
st.booleans(),
),
min_size=1,
)
.map("".join)
.map("#{}".format)
)

return st.builds(
"{}://{}{}/{}{}".format,
schemes,
domains(),
st.just("") | ports,
paths,
st.just("") | fragments,
st.just("") | _url_fragments_strategy,
)
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def do_validate(self):

def calc_label(self):
return combine_labels(
self.class_label, *[s.label for s in self.element_strategies]
self.class_label, *(s.label for s in self.element_strategies)
)

def __repr__(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,7 @@ def __init__(self, target, args, kwargs):
def do_draw(self, data):
try:
return self.target(
*[data.draw(a) for a in self.args],
*(data.draw(a) for a in self.args),
**{k: data.draw(v) for k, v in self.kwargs.items()},
)
except TypeError as err:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ def element_strategies(self):

def calc_label(self):
return combine_labels(
self.class_label, *[p.label for p in self.original_strategies]
self.class_label, *(p.label for p in self.original_strategies)
)

def do_draw(self, data: ConjectureData) -> Ex:
Expand Down
10 changes: 8 additions & 2 deletions hypothesis-python/tests/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from hypothesis._settings import Phase
from hypothesis.errors import HypothesisDeprecationWarning
from hypothesis.internal.entropy import deterministic_PRNG
from hypothesis.internal.reflection import proxies
from hypothesis.reporting import default, with_reporter
from hypothesis.strategies._internal.core import from_type, register_type_strategy
Expand Down Expand Up @@ -94,8 +95,13 @@ def fails_with(e):
def accepts(f):
@proxies(f)
def inverted_test(*arguments, **kwargs):
with raises(e):
f(*arguments, **kwargs)
# Most of these expected-failure tests are non-deterministic, so
# we rig the PRNG to avoid occasional flakiness. We do this outside
# the `raises` context manager so that any problems in rigging the
# PRNG don't accidentally count as the expected failure.
with deterministic_PRNG():
with raises(e):
f(*arguments, **kwargs)

return inverted_test

Expand Down
24 changes: 22 additions & 2 deletions hypothesis-python/tests/cover/test_provisional_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@

import pytest

from hypothesis import given
from hypothesis import given, settings
from hypothesis.errors import InvalidArgument
from hypothesis.provisional import domains, urls
from hypothesis.provisional import (
FRAGMENT_SAFE_CHARACTERS,
_url_fragments_strategy,
domains,
urls,
)

from tests.common.debug import find_any

Expand Down Expand Up @@ -63,3 +68,18 @@ def test_valid_domains_arguments(max_length, max_element_length):
@pytest.mark.parametrize("strategy", [domains(), urls()])
def test_find_any_non_empty(strategy):
find_any(strategy, lambda s: len(s) > 0)


@given(_url_fragments_strategy)
# There's a lambda in the implementation that only gets run if we generate at
# least one percent-escape sequence, so we derandomize to ensure that coverage
# isn't flaky.
@settings(derandomize=True)
def test_url_fragments_contain_legal_chars(fragment):
assert fragment.startswith("#")

# Strip all legal escape sequences. Any remaining % characters were not
# part of a legal escape sequence.
without_escapes = re.sub(r"(?ai)%[0-9a-f][0-9a-f]", "", fragment[1:])

assert set(without_escapes).issubset(FRAGMENT_SAFE_CHARACTERS)
2 changes: 1 addition & 1 deletion requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ attrs==21.2.0
# via
# hypothesis (hypothesis-python/setup.py)
# pytest
execnet==1.8.0
execnet==1.8.1
# via pytest-xdist
iniconfig==1.1.1
# via pytest
Expand Down
12 changes: 6 additions & 6 deletions requirements/tools.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ decorator==5.0.9
# via
# ipython
# traitlets
distlib==0.3.1
distlib==0.3.2
# via virtualenv
django==3.2.3
# via -r requirements/tools.in
Expand Down Expand Up @@ -107,7 +107,7 @@ idna==2.10
# via requests
imagesize==1.2.0
# via sphinx
importlib-metadata==4.0.1
importlib-metadata==4.3.1
# via
# keyring
# twine
Expand Down Expand Up @@ -213,7 +213,7 @@ pytz==2021.1
# via
# babel
# django
pyupgrade==2.18.1
pyupgrade==2.19.0
# via shed
pyyaml==5.4.1
# via
Expand Down Expand Up @@ -293,7 +293,7 @@ toml==0.10.2
# tox
tox==3.23.1
# via -r requirements/tools.in
tqdm==4.60.0
tqdm==4.61.0
# via twine
traitlets==4.3.3
# via
Expand All @@ -310,9 +310,9 @@ typing-extensions==3.10.0.0
# typing-inspect
typing-inspect==0.6.0
# via libcst
urllib3==1.26.4
urllib3==1.26.5
# via requests
virtualenv==20.4.6
virtualenv==20.4.7
# via tox
wcwidth==0.2.5
# via prompt-toolkit
Expand Down
6 changes: 3 additions & 3 deletions tooling/src/hypothesistooling/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ def codespell(*files):
def lint():
pip_tool(
"flake8",
*[f for f in tools.all_files() if f.endswith(".py")],
*(f for f in tools.all_files() if f.endswith(".py")),
"--config",
os.path.join(tools.ROOT, ".flake8"),
)
codespell(*[f for f in tools.all_files() if not f.endswith("by-domain.txt")])
codespell(*(f for f in tools.all_files() if not f.endswith("by-domain.txt")))


HEAD = tools.hash_for_name("HEAD")
Expand Down Expand Up @@ -364,7 +364,7 @@ def run_tox(task, version):
PY39 = "3.9.5"
PY310 = "3.10-dev"
PYPY36 = "pypy3.6-7.3.3"
PYPY37 = "pypy3.7-7.3.4"
PYPY37 = "pypy3.7-7.3.5"


# ALIASES are the executable names for each Python version
Expand Down
2 changes: 1 addition & 1 deletion whole-repo-tests/test_rst_is_valid.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def is_sphinx(f):


def test_passes_rst_lint():
pip_tool("rst-lint", *[f for f in ALL_RST if not is_sphinx(f)])
pip_tool("rst-lint", *(f for f in ALL_RST if not is_sphinx(f)))


def test_passes_flake8():
Expand Down

0 comments on commit b12dbe6

Please sign in to comment.