Skip to content

Commit b12dbe6

Browse files
authored
Merge pull request #2995 from Zalathar/url-fragments-coverage
2 parents 126ce20 + 03085f6 commit b12dbe6

File tree

14 files changed

+74
-37
lines changed

14 files changed

+74
-37
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
RELEASE_TYPE: patch
2+
3+
This release adjusts some internal code to help make our test suite more
4+
reliable.
5+
6+
There is no user-visible change.

hypothesis-python/src/hypothesis/extra/django/_fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ def _for_text(field):
233233
# Not maximally efficient, but it makes pathological cases rarer.
234234
# If you want a challenge: extend https://qntm.org/greenery to
235235
# compute intersections of the full Python regex language.
236-
return st.one_of(*[st.from_regex(r) for r in regexes])
236+
return st.one_of(*(st.from_regex(r) for r in regexes))
237237
# If there are no (usable) regexes, we use a standard text strategy.
238238
min_size, max_size = length_bounds_from_validators(field)
239239
strategy = st.text(

hypothesis-python/src/hypothesis/extra/numpy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1489,5 +1489,5 @@ def array_for(index_shape, size):
14891489
)
14901490

14911491
return result_shape.flatmap(
1492-
lambda index_shape: st.tuples(*[array_for(index_shape, size) for size in shape])
1492+
lambda index_shape: st.tuples(*(array_for(index_shape, size) for size in shape))
14931493
)

hypothesis-python/src/hypothesis/internal/filtering.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def numeric_bounds_from_ast(
180180

181181
if isinstance(tree, ast.BoolOp) and isinstance(tree.op, ast.And):
182182
return merge_preds(
183-
*[numeric_bounds_from_ast(node, argname, fallback) for node in tree.values]
183+
*(numeric_bounds_from_ast(node, argname, fallback) for node in tree.values)
184184
)
185185

186186
return fallback

hypothesis-python/src/hypothesis/provisional.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def do_draw(self, data):
112112
.filter(lambda tld: len(tld) + 2 <= self.max_length)
113113
.flatmap(
114114
lambda tld: st.tuples(
115-
*[st.sampled_from([c.lower(), c.upper()]) for c in tld]
115+
*(st.sampled_from([c.lower(), c.upper()]) for c in tld)
116116
).map("".join)
117117
)
118118
)
@@ -142,6 +142,25 @@ def domains(
142142
)
143143

144144

145+
# The `urls()` strategy uses this to generate URL fragments (e.g. "#foo").
146+
# It has been extracted to top-level so that we can test it independently
147+
# of `urls()`, which helps with getting non-flaky coverage of the lambda.
148+
_url_fragments_strategy = (
149+
st.lists(
150+
st.builds(
151+
lambda char, encode: f"%{ord(char):02X}"
152+
if (encode or char not in FRAGMENT_SAFE_CHARACTERS)
153+
else char,
154+
st.characters(min_codepoint=0, max_codepoint=255),
155+
st.booleans(),
156+
),
157+
min_size=1,
158+
)
159+
.map("".join)
160+
.map("#{}".format)
161+
)
162+
163+
145164
@defines_strategy(force_reusable_values=True)
146165
def urls() -> st.SearchStrategy[str]:
147166
"""A strategy for :rfc:`3986`, generating http/https URLs."""
@@ -152,26 +171,12 @@ def url_encode(s):
152171
schemes = st.sampled_from(["http", "https"])
153172
ports = st.integers(min_value=0, max_value=2 ** 16 - 1).map(":{}".format)
154173
paths = st.lists(st.text(string.printable).map(url_encode)).map("/".join)
155-
fragments = (
156-
st.lists(
157-
st.builds(
158-
lambda char, encode: f"%{ord(char):02X}"
159-
if (encode or char not in FRAGMENT_SAFE_CHARACTERS)
160-
else char,
161-
st.characters(min_codepoint=0, max_codepoint=255),
162-
st.booleans(),
163-
),
164-
min_size=1,
165-
)
166-
.map("".join)
167-
.map("#{}".format)
168-
)
169174

170175
return st.builds(
171176
"{}://{}{}/{}{}".format,
172177
schemes,
173178
domains(),
174179
st.just("") | ports,
175180
paths,
176-
st.just("") | fragments,
181+
st.just("") | _url_fragments_strategy,
177182
)

hypothesis-python/src/hypothesis/strategies/_internal/collections.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def do_validate(self):
4040

4141
def calc_label(self):
4242
return combine_labels(
43-
self.class_label, *[s.label for s in self.element_strategies]
43+
self.class_label, *(s.label for s in self.element_strategies)
4444
)
4545

4646
def __repr__(self):

hypothesis-python/src/hypothesis/strategies/_internal/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ def __init__(self, target, args, kwargs):
762762
def do_draw(self, data):
763763
try:
764764
return self.target(
765-
*[data.draw(a) for a in self.args],
765+
*(data.draw(a) for a in self.args),
766766
**{k: data.draw(v) for k, v in self.kwargs.items()},
767767
)
768768
except TypeError as err:

hypothesis-python/src/hypothesis/strategies/_internal/strategies.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ def element_strategies(self):
638638

639639
def calc_label(self):
640640
return combine_labels(
641-
self.class_label, *[p.label for p in self.original_strategies]
641+
self.class_label, *(p.label for p in self.original_strategies)
642642
)
643643

644644
def do_draw(self, data: ConjectureData) -> Ex:

hypothesis-python/tests/common/utils.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from hypothesis._settings import Phase
2121
from hypothesis.errors import HypothesisDeprecationWarning
22+
from hypothesis.internal.entropy import deterministic_PRNG
2223
from hypothesis.internal.reflection import proxies
2324
from hypothesis.reporting import default, with_reporter
2425
from hypothesis.strategies._internal.core import from_type, register_type_strategy
@@ -94,8 +95,13 @@ def fails_with(e):
9495
def accepts(f):
9596
@proxies(f)
9697
def inverted_test(*arguments, **kwargs):
97-
with raises(e):
98-
f(*arguments, **kwargs)
98+
# Most of these expected-failure tests are non-deterministic, so
99+
# we rig the PRNG to avoid occasional flakiness. We do this outside
100+
# the `raises` context manager so that any problems in rigging the
101+
# PRNG don't accidentally count as the expected failure.
102+
with deterministic_PRNG():
103+
with raises(e):
104+
f(*arguments, **kwargs)
99105

100106
return inverted_test
101107

hypothesis-python/tests/cover/test_provisional_strategies.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@
1818

1919
import pytest
2020

21-
from hypothesis import given
21+
from hypothesis import given, settings
2222
from hypothesis.errors import InvalidArgument
23-
from hypothesis.provisional import domains, urls
23+
from hypothesis.provisional import (
24+
FRAGMENT_SAFE_CHARACTERS,
25+
_url_fragments_strategy,
26+
domains,
27+
urls,
28+
)
2429

2530
from tests.common.debug import find_any
2631

@@ -63,3 +68,18 @@ def test_valid_domains_arguments(max_length, max_element_length):
6368
@pytest.mark.parametrize("strategy", [domains(), urls()])
6469
def test_find_any_non_empty(strategy):
6570
find_any(strategy, lambda s: len(s) > 0)
71+
72+
73+
@given(_url_fragments_strategy)
74+
# There's a lambda in the implementation that only gets run if we generate at
75+
# least one percent-escape sequence, so we derandomize to ensure that coverage
76+
# isn't flaky.
77+
@settings(derandomize=True)
78+
def test_url_fragments_contain_legal_chars(fragment):
79+
assert fragment.startswith("#")
80+
81+
# Strip all legal escape sequences. Any remaining % characters were not
82+
# part of a legal escape sequence.
83+
without_escapes = re.sub(r"(?ai)%[0-9a-f][0-9a-f]", "", fragment[1:])
84+
85+
assert set(without_escapes).issubset(FRAGMENT_SAFE_CHARACTERS)

0 commit comments

Comments
 (0)