Skip to content

Commit

Permalink
Merge branch 'master' into explain-ir
Browse files Browse the repository at this point in the history
  • Loading branch information
tybug committed Nov 8, 2024
1 parent eedbd9a commit 3fd3f06
Show file tree
Hide file tree
Showing 34 changed files with 443 additions and 217 deletions.
38 changes: 38 additions & 0 deletions hypothesis-python/docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,44 @@ Hypothesis 6.x

.. include:: ../RELEASE.rst

.. _v6.118.0:

--------------------
6.118.0 - 2024-11-08
--------------------

The :func:`~hypothesis.provisional.urls` strategy no longer generates
URLs where the port number is 0.

This change is motivated by the idea that the generated URLs should, at least in
theory, be possible to fetch. The port number 0 is special; if a server binds to
port 0, the kernel will allocate an unused, and non-zero, port instead. That
means that it's not possible for a server to actually be listening on port 0.
This motivation is briefly described in the documentation for
:func:`~hypothesis.provisional.urls`.

Fixes :issue:`4157`.

Thanks to @gmacon for this contribution!

.. _v6.117.0:

--------------------
6.117.0 - 2024-11-07
--------------------

This changes the behaviour of settings profiles so that if you reregister the currently loaded profile it will automatically reload it. Previously you would have had to load it again.

In particular this means that if you register a "ci" profile, it will automatically be used when Hypothesis detects you are running on CI.

.. _v6.116.0:

--------------------
6.116.0 - 2024-11-01
--------------------

Hypothesis now detects if it is running on a CI server and provides better default settings for running on CI in this case.

.. _v6.115.6:

--------------------
Expand Down
22 changes: 22 additions & 0 deletions hypothesis-python/docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,28 @@ by your conftest you can load one with the command line option ``--hypothesis-pr
$ pytest tests --hypothesis-profile <profile-name>
Hypothesis comes with two built-in profiles, ``ci`` and ``default``.
``ci`` is set up to have good defaults for running in a CI environment, so emphasizes determinism, while the
``default`` settings are picked to be more likely to find bugs and to have a good workflow when used for local development.

Hypothesis will automatically detect certain common CI environments and use the ci profile automatically
when running in them.
In particular, if you wish to use the ``ci`` profile, setting the ``CI`` environment variable will do this.

This will still be the case if you register your own ci profile. For example, if you wanted to run more examples in CI, you might configure it as follows:

.. code-block:: python
settings.register_profile(
"ci",
settings(
settings.get_profile("ci"),
max_examples=1000,
),
)
This will configure your CI to run 1000 examples per test rather than the default of 100, and this change will automatically be picked up when running on a CI server.

.. _healthchecks:

-------------
Expand Down
35 changes: 31 additions & 4 deletions hypothesis-python/src/hypothesis/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def __get__(self, obj, type=None):
from hypothesis.database import ExampleDatabase

result = ExampleDatabase(not_set)
assert result is not not_set
return result
except KeyError:
raise AttributeError(self.name) from None
Expand Down Expand Up @@ -96,7 +97,7 @@ def default(cls):
v = default_variable.value
if v is not None:
return v
if hasattr(settings, "_current_profile"):
if getattr(settings, "_current_profile", None) is not None:
settings.load_profile(settings._current_profile)
assert default_variable.value is not None
return default_variable.value
Expand Down Expand Up @@ -131,6 +132,7 @@ class settings(metaclass=settingsMeta):
__definitions_are_locked = False
_profiles: ClassVar[dict[str, "settings"]] = {}
__module__ = "hypothesis"
_current_profile = None

def __getattr__(self, name):
if name in all_settings:
Expand Down Expand Up @@ -315,9 +317,15 @@ def register_profile(
:class:`~hypothesis.settings`: optional ``parent`` settings, and
keyword arguments for each setting that will be set differently to
parent (or settings.default, if parent is None).
If you register a profile that has already been defined and that profile
is the currently loaded profile, the new changes will take effect immediately,
and do not require reloading the profile.
"""
check_type(str, name, "name")
settings._profiles[name] = settings(parent=parent, **kwargs)
if settings._current_profile == name:
settings.load_profile(name)

@staticmethod
def get_profile(name: str) -> "settings":
Expand Down Expand Up @@ -407,6 +415,8 @@ def _max_examples_validator(x):
:ref:`separate settings profiles <settings_profiles>` - for example running
quick deterministic tests on every commit, and a longer non-deterministic
nightly testing run.
By default when running on CI, this will be set to True.
""",
)

Expand Down Expand Up @@ -682,6 +692,8 @@ def _validate_deadline(x):
variability in test run time).
Set this to ``None`` to disable this behaviour entirely.
By default when running on CI, this will be set to None.
""",
)

Expand All @@ -694,13 +706,11 @@ def is_in_ci() -> bool:

settings._define_setting(
"print_blob",
default=is_in_ci(),
show_default=False,
default=False,
options=(True, False),
description="""
If set to ``True``, Hypothesis will print code for failing examples that can be used with
:func:`@reproduce_failure <hypothesis.reproduce_failure>` to reproduce the failing example.
The default is ``True`` if the ``CI`` or ``TF_BUILD`` env vars are set, ``False`` otherwise.
""",
)

Expand Down Expand Up @@ -750,6 +760,23 @@ def note_deprecation(

settings.register_profile("default", settings())
settings.load_profile("default")

assert settings.default is not None

CI = settings(
derandomize=True,
deadline=None,
database=None,
print_blob=True,
suppress_health_check=[HealthCheck.too_slow],
)

settings.register_profile("ci", CI)


if is_in_ci():
settings.load_profile("ci")

assert settings.default is not None


Expand Down
33 changes: 16 additions & 17 deletions hypothesis-python/src/hypothesis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1564,23 +1564,23 @@ def wrapped_test(*arguments, **kwargs):
"to ensure that each example is run in a separate "
"database transaction."
)
if settings.database is not None:
nonlocal prev_self
# Check selfy really is self (not e.g. a mock) before we health-check
cur_self = (
stuff.selfy
if getattr(type(stuff.selfy), test.__name__, None) is wrapped_test
else None

nonlocal prev_self
# Check selfy really is self (not e.g. a mock) before we health-check
cur_self = (
stuff.selfy
if getattr(type(stuff.selfy), test.__name__, None) is wrapped_test
else None
)
if prev_self is Unset:
prev_self = cur_self
elif cur_self is not prev_self:
msg = (
f"The method {test.__qualname__} was called from multiple "
"different executors. This may lead to flaky tests and "
"nonreproducible errors when replaying from database."
)
if prev_self is Unset:
prev_self = cur_self
elif cur_self is not prev_self:
msg = (
f"The method {test.__qualname__} was called from multiple "
"different executors. This may lead to flaky tests and "
"nonreproducible errors when replaying from database."
)
fail_health_check(settings, msg, HealthCheck.differing_executors)
fail_health_check(settings, msg, HealthCheck.differing_executors)

state = StateForActualGivenExecution(
stuff, test, settings, random, wrapped_test
Expand Down Expand Up @@ -1675,7 +1675,6 @@ def wrapped_test(*arguments, **kwargs):
# The exception caught here should either be an actual test
# failure (or BaseExceptionGroup), or some kind of fatal error
# that caused the engine to stop.

generated_seed = wrapped_test._hypothesis_internal_use_generated_seed
with local_settings(settings):
if not (state.failed_normally or generated_seed is None):
Expand Down
9 changes: 7 additions & 2 deletions hypothesis-python/src/hypothesis/provisional.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,18 @@ def domains(

@defines_strategy(force_reusable_values=True)
def urls() -> st.SearchStrategy[str]:
"""A strategy for :rfc:`3986`, generating http/https URLs."""
"""A strategy for :rfc:`3986`, generating http/https URLs.
The generated URLs could, at least in theory, be passed to an HTTP client
and fetched.
"""

def url_encode(s: str) -> str:
return "".join(c if c in URL_SAFE_CHARACTERS else "%%%02X" % ord(c) for c in s)

schemes = st.sampled_from(["http", "https"])
ports = st.integers(min_value=0, max_value=2**16 - 1).map(":{}".format)
ports = st.integers(min_value=1, max_value=2**16 - 1).map(":{}".format)
paths = st.lists(st.text(string.printable).map(url_encode)).map("/".join)

return st.builds(
Expand Down
2 changes: 1 addition & 1 deletion hypothesis-python/src/hypothesis/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.

__version_info__ = (6, 115, 6)
__version_info__ = (6, 118, 0)
__version__ = ".".join(map(str, __version_info__))
7 changes: 4 additions & 3 deletions hypothesis-python/tests/common/costbounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ def find_integer_cost(n):
except KeyError:
pass

cost = [0]
cost = 0

def test(i):
cost[0] += 1
nonlocal cost
cost += 1
return i <= n

find_integer(test)

return FIND_INTEGER_COSTS.setdefault(n, cost[0])
return FIND_INTEGER_COSTS.setdefault(n, cost)
7 changes: 4 additions & 3 deletions hypothesis-python/tests/common/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from warnings import filterwarnings

from hypothesis import HealthCheck, Phase, Verbosity, settings
from hypothesis._settings import not_set
from hypothesis._settings import CI, is_in_ci, not_set
from hypothesis.internal.conjecture.data import AVAILABLE_PROVIDERS
from hypothesis.internal.coverage import IN_COVERAGE_TESTS

Expand Down Expand Up @@ -45,13 +45,14 @@ def run():
v = getattr(x, s.name)
# Check if it has a dynamically defined default and if so skip comparison.
if getattr(settings, s.name).show_default:
assert (
v == s.default
assert v == s.default or (
is_in_ci() and v == getattr(CI, s.name)
), f"({v!r} == x.{s.name}) != (s.{s.name} == {s.default!r})"

settings.register_profile(
"default",
settings(
settings.get_profile("default"),
max_examples=20 if IN_COVERAGE_TESTS else not_set,
phases=list(Phase), # Dogfooding the explain phase
),
Expand Down
27 changes: 16 additions & 11 deletions hypothesis-python/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,23 @@ def _consistently_increment_time(monkeypatch):
Replacing time with a fake version under our control avoids this problem.
"""
frozen = [False]
frozen = False

current_time = [time_module.time()]
current_time = time_module.time()

def time():
if not frozen[0]:
current_time[0] += TIME_INCREMENT
return current_time[0]
nonlocal current_time
if not frozen:
current_time += TIME_INCREMENT
return current_time

def sleep(naptime):
current_time[0] += naptime
nonlocal current_time
current_time += naptime

def freeze():
frozen[0] = True
nonlocal frozen
frozen = True

def _patch(name, fn):
monkeypatch.setattr(time_module, name, wraps(getattr(time_module, name))(fn))
Expand All @@ -119,14 +122,16 @@ def _patch(name, fn):
# ensure timer callback is added, then bracket it by freeze/unfreeze below
junkdrawer.gc_cumulative_time()

_was_frozen = [False]
_was_frozen = False

def _freezer(*_):
_was_frozen[0] = frozen[0]
frozen[0] = True
nonlocal _was_frozen, frozen
_was_frozen = frozen
frozen = True

def _unfreezer(*_):
frozen[0] = _was_frozen[0]
nonlocal _was_frozen, frozen
frozen = _was_frozen

gc.callbacks.insert(0, _freezer) # freeze before gc callback
gc.callbacks.append(_unfreezer) # unfreeze after
Expand Down
Loading

0 comments on commit 3fd3f06

Please sign in to comment.